llm.rb 8.0.0 → 9.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +165 -2
  3. data/README.md +161 -509
  4. data/data/bedrock.json +2948 -0
  5. data/data/deepseek.json +8 -8
  6. data/data/openai.json +39 -2
  7. data/data/xai.json +35 -0
  8. data/data/zai.json +1 -1
  9. data/lib/llm/active_record/acts_as_llm.rb +7 -8
  10. data/lib/llm/agent.rb +36 -16
  11. data/lib/llm/context.rb +30 -26
  12. data/lib/llm/contract/completion.rb +45 -0
  13. data/lib/llm/cost.rb +81 -4
  14. data/lib/llm/error.rb +1 -1
  15. data/lib/llm/function/array.rb +8 -5
  16. data/lib/llm/function/call_group.rb +39 -0
  17. data/lib/llm/function/fork/task.rb +6 -0
  18. data/lib/llm/function/ractor/task.rb +6 -0
  19. data/lib/llm/function/task.rb +10 -0
  20. data/lib/llm/function.rb +1 -0
  21. data/lib/llm/mcp/transport/http.rb +26 -46
  22. data/lib/llm/mcp/transport/stdio.rb +0 -8
  23. data/lib/llm/mcp.rb +6 -23
  24. data/lib/llm/object.rb +8 -0
  25. data/lib/llm/provider.rb +29 -19
  26. data/lib/llm/providers/anthropic/error_handler.rb +6 -7
  27. data/lib/llm/providers/anthropic/files.rb +2 -2
  28. data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
  29. data/lib/llm/providers/anthropic.rb +1 -1
  30. data/lib/llm/providers/bedrock/error_handler.rb +79 -0
  31. data/lib/llm/providers/bedrock/models.rb +109 -0
  32. data/lib/llm/providers/bedrock/request_adapter/completion.rb +153 -0
  33. data/lib/llm/providers/bedrock/request_adapter.rb +95 -0
  34. data/lib/llm/providers/bedrock/response_adapter/completion.rb +173 -0
  35. data/lib/llm/providers/bedrock/response_adapter/models.rb +34 -0
  36. data/lib/llm/providers/bedrock/response_adapter.rb +40 -0
  37. data/lib/llm/providers/bedrock/signature.rb +166 -0
  38. data/lib/llm/providers/bedrock/stream_decoder.rb +140 -0
  39. data/lib/llm/providers/bedrock/stream_parser.rb +201 -0
  40. data/lib/llm/providers/bedrock.rb +272 -0
  41. data/lib/llm/providers/google/error_handler.rb +6 -7
  42. data/lib/llm/providers/google/files.rb +2 -4
  43. data/lib/llm/providers/google/images.rb +1 -1
  44. data/lib/llm/providers/google/models.rb +0 -2
  45. data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
  46. data/lib/llm/providers/google.rb +1 -1
  47. data/lib/llm/providers/ollama/error_handler.rb +6 -7
  48. data/lib/llm/providers/ollama/models.rb +0 -2
  49. data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
  50. data/lib/llm/providers/ollama.rb +1 -1
  51. data/lib/llm/providers/openai/audio.rb +3 -3
  52. data/lib/llm/providers/openai/error_handler.rb +6 -7
  53. data/lib/llm/providers/openai/files.rb +2 -2
  54. data/lib/llm/providers/openai/images.rb +3 -3
  55. data/lib/llm/providers/openai/models.rb +1 -1
  56. data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
  57. data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
  58. data/lib/llm/providers/openai/responses.rb +2 -2
  59. data/lib/llm/providers/openai/vector_stores.rb +1 -1
  60. data/lib/llm/providers/openai.rb +1 -1
  61. data/lib/llm/response.rb +10 -8
  62. data/lib/llm/sequel/plugin.rb +7 -8
  63. data/lib/llm/stream/queue.rb +15 -42
  64. data/lib/llm/stream.rb +4 -4
  65. data/lib/llm/transport/execution.rb +67 -0
  66. data/lib/llm/transport/http.rb +134 -0
  67. data/lib/llm/transport/persistent_http.rb +152 -0
  68. data/lib/llm/transport/response/http.rb +113 -0
  69. data/lib/llm/transport/response.rb +112 -0
  70. data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
  71. data/lib/llm/transport.rb +139 -0
  72. data/lib/llm/usage.rb +14 -5
  73. data/lib/llm/version.rb +1 -1
  74. data/lib/llm.rb +10 -12
  75. data/llm.gemspec +2 -16
  76. metadata +23 -19
  77. data/lib/llm/provider/transport/http/execution.rb +0 -115
  78. data/lib/llm/provider/transport/http/interruptible.rb +0 -114
  79. data/lib/llm/provider/transport/http.rb +0 -145
  80. data/lib/llm/utils.rb +0 -19
@@ -33,6 +33,36 @@ module LLM::Google::ResponseAdapter
33
33
  body.usageMetadata.thoughtsTokenCount || 0
34
34
  end
35
35
 
36
+ ##
37
+ # (see LLM::Contract::Completion#input_audio_tokens)
38
+ def input_audio_tokens
39
+ super
40
+ end
41
+
42
+ ##
43
+ # (see LLM::Contract::Completion#output_audio_tokens)
44
+ def output_audio_tokens
45
+ super
46
+ end
47
+
48
+ ##
49
+ # (see LLM::Contract::Completion#input_image_tokens)
50
+ def input_image_tokens
51
+ super
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#cache_read_tokens)
56
+ def cache_read_tokens
57
+ 0
58
+ end
59
+
60
+ ##
61
+ # (see LLM::Contract::Completion#cache_write_tokens)
62
+ def cache_write_tokens
63
+ 0
64
+ end
65
+
36
66
  ##
37
67
  # (see LLM::Contract::Completion#total_tokens)
38
68
  def total_tokens
@@ -208,7 +208,7 @@ module LLM
208
208
  req = Net::HTTP::Post.new(path, headers)
209
209
  messages = build_complete_messages(prompt, params, role)
210
210
  body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
211
- set_body_stream(req, StringIO.new(body))
211
+ transport.set_body_stream(req, StringIO.new(body))
212
212
  req
213
213
  end
214
214
 
@@ -5,7 +5,7 @@ class LLM::Ollama
5
5
  # @private
6
6
  class ErrorHandler
7
7
  ##
8
- # @return [Net::HTTPResponse]
8
+ # @return [LLM::Transport::Response]
9
9
  # Non-2XX response from the server
10
10
  attr_reader :res
11
11
 
@@ -19,13 +19,13 @@ class LLM::Ollama
19
19
  # The tracer
20
20
  # @param [Object, nil] span
21
21
  # The span
22
- # @param [Net::HTTPResponse] res
22
+ # @param [LLM::Transport::Response, Net::HTTPResponse] res
23
23
  # The response from the server
24
24
  # @return [LLM::Ollama::ErrorHandler]
25
25
  def initialize(tracer, span, res)
26
26
  @tracer = tracer
27
27
  @span = span
28
- @res = res
28
+ @res = LLM::Transport::Response.from(res)
29
29
  end
30
30
 
31
31
  ##
@@ -43,12 +43,11 @@ class LLM::Ollama
43
43
  ##
44
44
  # @return [LLM::Error]
45
45
  def error
46
- case res
47
- when Net::HTTPServerError
46
+ if res.server_error?
48
47
  LLM::ServerError.new("Server error").tap { _1.response = res }
49
- when Net::HTTPUnauthorized
48
+ elsif res.unauthorized?
50
49
  LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
51
- when Net::HTTPTooManyRequests
50
+ elsif res.rate_limited?
52
51
  LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
53
52
  else
54
53
  LLM::Error.new("Unexpected response").tap { _1.response = res }
@@ -17,8 +17,6 @@ class LLM::Ollama
17
17
  # print "id: ", model.id, "\n"
18
18
  # end
19
19
  class Models
20
- include LLM::Utils
21
-
22
20
  ##
23
21
  # Returns a new Models object
24
22
  # @param provider [LLM::Provider]
@@ -27,6 +27,36 @@ module LLM::Ollama::ResponseAdapter
27
27
  0
28
28
  end
29
29
 
30
+ ##
31
+ # (see LLM::Contract::Completion#input_audio_tokens)
32
+ def input_audio_tokens
33
+ super
34
+ end
35
+
36
+ ##
37
+ # (see LLM::Contract::Completion#output_audio_tokens)
38
+ def output_audio_tokens
39
+ super
40
+ end
41
+
42
+ ##
43
+ # (see LLM::Contract::Completion#input_image_tokens)
44
+ def input_image_tokens
45
+ super
46
+ end
47
+
48
+ ##
49
+ # (see LLM::Contract::Completion#cache_read_tokens)
50
+ def cache_read_tokens
51
+ 0
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#cache_write_tokens)
56
+ def cache_write_tokens
57
+ 0
58
+ end
59
+
30
60
  ##
31
61
  # (see LLM::Contract::Completion#total_tokens)
32
62
  def total_tokens
@@ -130,7 +130,7 @@ module LLM
130
130
  messages = build_complete_messages(prompt, params, role)
131
131
  body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
132
132
  req = Net::HTTP::Post.new("/api/chat", headers)
133
- set_body_stream(req, StringIO.new(body))
133
+ transport.set_body_stream(req, StringIO.new(body))
134
134
  req
135
135
  end
136
136
 
@@ -57,7 +57,7 @@ class LLM::OpenAI
57
57
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), model:))
58
58
  req = Net::HTTP::Post.new(path("/audio/transcriptions"), headers)
59
59
  req["content-type"] = multi.content_type
60
- set_body_stream(req, multi.body)
60
+ transport.set_body_stream(req, multi.body)
61
61
  res, span, tracer = execute(request: req, operation: "request")
62
62
  res = LLM::Response.new(res)
63
63
  tracer.on_request_finish(operation: "request", model:, res:, span:)
@@ -81,7 +81,7 @@ class LLM::OpenAI
81
81
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), model:))
82
82
  req = Net::HTTP::Post.new(path("/audio/translations"), headers)
83
83
  req["content-type"] = multi.content_type
84
- set_body_stream(req, multi.body)
84
+ transport.set_body_stream(req, multi.body)
85
85
  res, span, tracer = execute(request: req, operation: "request")
86
86
  res = LLM::Response.new(res)
87
87
  tracer.on_request_finish(operation: "request", model:, res:, span:)
@@ -90,7 +90,7 @@ class LLM::OpenAI
90
90
 
91
91
  private
92
92
 
93
- [:path, :headers, :execute, :set_body_stream].each do |m|
93
+ [:path, :headers, :execute, :transport].each do |m|
94
94
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
95
95
  end
96
96
  end
@@ -5,7 +5,7 @@ class LLM::OpenAI
5
5
  # @private
6
6
  class ErrorHandler
7
7
  ##
8
- # @return [Net::HTTPResponse]
8
+ # @return [LLM::Transport::Response]
9
9
  # Non-2XX response from the server
10
10
  attr_reader :res
11
11
 
@@ -19,13 +19,13 @@ class LLM::OpenAI
19
19
  # The tracer
20
20
  # @param [Object, nil] span
21
21
  # The span
22
- # @param [Net::HTTPResponse] res
22
+ # @param [LLM::Transport::Response, Net::HTTPResponse] res
23
23
  # The response from the server
24
24
  # @return [LLM::OpenAI::ErrorHandler]
25
25
  def initialize(tracer, span, res)
26
26
  @tracer = tracer
27
27
  @span = span
28
- @res = res
28
+ @res = LLM::Transport::Response.from(res)
29
29
  end
30
30
 
31
31
  ##
@@ -49,12 +49,11 @@ class LLM::OpenAI
49
49
  ##
50
50
  # @return [LLM::Error]
51
51
  def error
52
- case res
53
- when Net::HTTPServerError
52
+ if res.server_error?
54
53
  LLM::ServerError.new("Server error").tap { _1.response = res }
55
- when Net::HTTPUnauthorized
54
+ elsif res.unauthorized?
56
55
  LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
57
- when Net::HTTPTooManyRequests
56
+ elsif res.rate_limited?
58
57
  LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
59
58
  else
60
59
  error = body["error"] || {}
@@ -62,7 +62,7 @@ class LLM::OpenAI
62
62
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), purpose:))
63
63
  req = Net::HTTP::Post.new(path("/files"), headers)
64
64
  req["content-type"] = multi.content_type
65
- set_body_stream(req, multi.body)
65
+ transport.set_body_stream(req, multi.body)
66
66
  res, span, tracer = execute(request: req, operation: "request")
67
67
  res = ResponseAdapter.adapt(res, type: :file)
68
68
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -134,7 +134,7 @@ class LLM::OpenAI
134
134
 
135
135
  private
136
136
 
137
- [:path, :headers, :execute, :set_body_stream].each do |m|
137
+ [:path, :headers, :execute, :transport].each do |m|
138
138
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
139
139
  end
140
140
  end
@@ -78,7 +78,7 @@ class LLM::OpenAI
78
78
  multi = LLM::Multipart.new(params.merge!(image:, model:, response_format:))
79
79
  req = Net::HTTP::Post.new(path("/images/variations"), headers)
80
80
  req["content-type"] = multi.content_type
81
- set_body_stream(req, multi.body)
81
+ transport.set_body_stream(req, multi.body)
82
82
  res, span, tracer = execute(request: req, operation: "request")
83
83
  res = ResponseAdapter.adapt(res, type: :image)
84
84
  tracer.on_request_finish(operation: "request", model:, res:, span:)
@@ -104,7 +104,7 @@ class LLM::OpenAI
104
104
  multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:, response_format:))
105
105
  req = Net::HTTP::Post.new(path("/images/edits"), headers)
106
106
  req["content-type"] = multi.content_type
107
- set_body_stream(req, multi.body)
107
+ transport.set_body_stream(req, multi.body)
108
108
  res, span, tracer = execute(request: req, operation: "request")
109
109
  res = ResponseAdapter.adapt(res, type: :image)
110
110
  tracer.on_request_finish(operation: "request", model:, res:, span:)
@@ -113,7 +113,7 @@ class LLM::OpenAI
113
113
 
114
114
  private
115
115
 
116
- [:path, :headers, :execute, :set_body_stream].each do |m|
116
+ [:path, :headers, :execute, :transport].each do |m|
117
117
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
118
118
  end
119
119
  end
@@ -48,7 +48,7 @@ class LLM::OpenAI
48
48
 
49
49
  private
50
50
 
51
- [:path, :headers, :execute, :set_body_stream].each do |m|
51
+ [:path, :headers, :execute].each do |m|
52
52
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
53
53
  end
54
54
  end
@@ -40,6 +40,48 @@ module LLM::OpenAI::ResponseAdapter
40
40
  &.reasoning_tokens || 0
41
41
  end
42
42
 
43
+ ##
44
+ # (see LLM::Contract::Completion#input_audio_tokens)
45
+ def input_audio_tokens
46
+ body
47
+ .usage
48
+ &.prompt_tokens_details
49
+ &.audio_tokens || 0
50
+ end
51
+
52
+ ##
53
+ # (see LLM::Contract::Completion#output_audio_tokens)
54
+ def output_audio_tokens
55
+ body
56
+ .usage
57
+ &.completion_tokens_details
58
+ &.audio_tokens || 0
59
+ end
60
+
61
+ ##
62
+ # (see LLM::Contract::Completion#input_image_tokens)
63
+ def input_image_tokens
64
+ body
65
+ .usage
66
+ &.prompt_tokens_details
67
+ &.image_tokens || 0
68
+ end
69
+
70
+ ##
71
+ # (see LLM::Contract::Completion#cache_read_tokens)
72
+ def cache_read_tokens
73
+ body
74
+ .usage
75
+ &.prompt_tokens_details
76
+ &.cached_tokens || 0
77
+ end
78
+
79
+ ##
80
+ # (see LLM::Contract::Completion#cache_write_tokens)
81
+ def cache_write_tokens
82
+ 0
83
+ end
84
+
43
85
  ##
44
86
  # (see LLM::Contract::Completion#total_tokens)
45
87
  def total_tokens
@@ -42,6 +42,45 @@ module LLM::OpenAI::ResponseAdapter
42
42
  &.reasoning_tokens || 0
43
43
  end
44
44
 
45
+ ##
46
+ # (see LLM::Contract::Completion#input_audio_tokens)
47
+ def input_audio_tokens
48
+ body
49
+ .usage
50
+ &.input_tokens_details
51
+ &.audio_tokens || 0
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#output_audio_tokens)
56
+ def output_audio_tokens
57
+ body
58
+ .usage
59
+ &.output_tokens_details
60
+ &.audio_tokens || 0
61
+ end
62
+
63
+ ##
64
+ # (see LLM::Contract::Completion#input_image_tokens)
65
+ def input_image_tokens
66
+ super
67
+ end
68
+
69
+ ##
70
+ # (see LLM::Contract::Completion#cache_read_tokens)
71
+ def cache_read_tokens
72
+ body
73
+ .usage
74
+ &.input_tokens_details
75
+ &.cached_tokens || 0
76
+ end
77
+
78
+ ##
79
+ # (see LLM::Contract::Completion#cache_write_tokens)
80
+ def cache_write_tokens
81
+ 0
82
+ end
83
+
45
84
  ##
46
85
  # (see LLM::Contract::Completion#total_tokens)
47
86
  def total_tokens
@@ -44,7 +44,7 @@ class LLM::OpenAI
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))
47
- set_body_stream(req, StringIO.new(body))
47
+ transport.set_body_stream(req, StringIO.new(body))
48
48
  res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
49
49
  res = ResponseAdapter.adapt(res, type: :responds)
50
50
  .extend(Module.new { define_method(:__tools__) { tools } })
@@ -85,7 +85,7 @@ class LLM::OpenAI
85
85
 
86
86
  private
87
87
 
88
- [:path, :headers, :execute, :set_body_stream, :resolve_tools].each do |m|
88
+ [:path, :headers, :execute, :transport, :resolve_tools].each do |m|
89
89
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
90
90
  end
91
91
 
@@ -259,7 +259,7 @@ class LLM::OpenAI
259
259
 
260
260
  private
261
261
 
262
- [:path, :headers, :execute, :set_body_stream].each do |m|
262
+ [:path, :headers, :execute].each do |m|
263
263
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
264
264
  end
265
265
  end
@@ -223,7 +223,7 @@ module LLM
223
223
  messages = build_complete_messages(prompt, params, role)
224
224
  body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
225
225
  req = Net::HTTP::Post.new(completions_path, headers)
226
- set_body_stream(req, StringIO.new(body))
226
+ transport.set_body_stream(req, StringIO.new(body))
227
227
  [req, messages]
228
228
  end
229
229
 
data/lib/llm/response.rb CHANGED
@@ -10,25 +10,27 @@ module LLM
10
10
  # handling can share one common surface without flattening away
11
11
  # specialized behavior.
12
12
  #
13
- # The normalized response still keeps the original
14
- # {Net::HTTPResponse Net::HTTPResponse} available through {#res}
15
- # when callers need direct access to raw HTTP details such as
16
- # headers, status codes, or unadapted bodies.
13
+ # The normalized response keeps the transport response available
14
+ # through {#res}. When the default net/http transport is in use,
15
+ # {LLM::Transport::Response::HTTP
16
+ # LLM::Transport::Response::HTTP} keeps the
17
+ # original {Net::HTTPResponse Net::HTTPResponse} available through
18
+ # its own {LLM::Transport::Response::HTTP#res #res}.
17
19
  class Response
18
20
  require "json"
19
21
 
20
22
  ##
21
23
  # Returns the HTTP response
22
- # @return [Net::HTTPResponse]
24
+ # @return [LLM::Transport::Response]
23
25
  attr_reader :res
24
26
 
25
27
  ##
26
- # @param [Net::HTTPResponse] res
28
+ # @param [LLM::Transport::Response] res
27
29
  # HTTP response
28
30
  # @return [LLM::Response]
29
31
  # Returns an instance of LLM::Response
30
32
  def initialize(res)
31
- @res = res
33
+ @res = LLM::Transport::Response.from(res)
32
34
  end
33
35
 
34
36
  ##
@@ -51,7 +53,7 @@ module LLM
51
53
  # Returns true if the response is successful
52
54
  # @return [Boolean]
53
55
  def ok?
54
- Net::HTTPSuccess === @res
56
+ @res.success?
55
57
  end
56
58
 
57
59
  ##
@@ -184,14 +184,6 @@ module LLM::Sequel
184
184
  ctx.wait(...)
185
185
  end
186
186
 
187
- ##
188
- # Calls into the stored context.
189
- # @see LLM::Context#call
190
- # @return [Object]
191
- def call(...)
192
- ctx.call(...)
193
- end
194
-
195
187
  ##
196
188
  # @see LLM::Context#mode
197
189
  # @return [Symbol]
@@ -222,6 +214,13 @@ module LLM::Sequel
222
214
  ctx.functions
223
215
  end
224
216
 
217
+ ##
218
+ # @see LLM::Context#functions?
219
+ # @return [Boolean]
220
+ def functions?
221
+ ctx.functions?
222
+ end
223
+
225
224
  ##
226
225
  # @see LLM::Context#returns
227
226
  # @return [Array<LLM::Function::Return>]
@@ -4,7 +4,7 @@ class LLM::Stream
4
4
  ##
5
5
  # A small queue for collecting streamed tool work. Values can be immediate
6
6
  # {LLM::Function::Return} objects or concurrent handles returned by
7
- # {LLM::Function#spawn}. Calling {#wait(strategy)} resolves queued work and
7
+ # {LLM::Function#spawn}. Calling {#wait} resolves queued work and
8
8
  # returns an array of {LLM::Function::Return} values.
9
9
  class Queue
10
10
  ##
@@ -41,56 +41,29 @@ class LLM::Stream
41
41
 
42
42
  ##
43
43
  # Waits for queued work to finish and returns function results.
44
- # @param [Symbol, Array<Symbol>] strategy
45
- # Controls concurrency strategy, or lists the possible concurrency strategies
46
- # to wait on:
47
- # - `:thread`: Use threads
48
- # - `:task`: Use async tasks (requires async gem)
49
- # - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
50
- # - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
51
- # - `[:thread, :ractor]`: Wait for any queued thread or ractor work, in the
52
- # given order. This is useful when different tools were spawned with
53
- # different concurrency strategies.
44
+ #
45
+ # Queued work is waited according to the actual task types that were
46
+ # enqueued, so callers do not need to provide a strategy here.
47
+ #
54
48
  # @return [Array<LLM::Function::Return>]
55
- def wait(strategy)
49
+ def wait
56
50
  returns, tasks = @items.shift(@items.length).partition { LLM::Function::Return === _1 }
57
- results = wait_tasks(tasks, strategy)
51
+ results = wait_tasks(tasks)
58
52
  returns.concat fire_hooks(tasks, results)
59
53
  end
60
54
  alias_method :value, :wait
61
55
 
62
56
  private
63
57
 
64
- def wait_tasks(tasks, strategy)
65
- strategies = Array(strategy)
66
- return wait_group(tasks, strategies.first) unless strategies.length > 1
67
- grouped = strategies.to_h { [_1, []] }
68
- tasks.each do |task|
69
- grouped[task_strategy(task)] << task
70
- end
71
- strategies.flat_map do |name|
72
- selected = grouped.fetch(name)
73
- selected.empty? ? [] : wait_group(selected, name)
74
- end
75
- end
76
-
77
- def wait_group(tasks, strategy)
78
- case strategy
79
- when :thread then LLM::Function::ThreadGroup.new(tasks).wait
80
- when :task then LLM::Function::TaskGroup.new(tasks).wait
81
- when :fiber then LLM::Function::FiberGroup.new(tasks).wait
82
- when :ractor then LLM::Function::Ractor::Group.new(tasks).wait
83
- else raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
84
- end
85
- end
86
-
87
- def task_strategy(task)
88
- case task.task
89
- when Thread then :thread
90
- when Fiber then :fiber
91
- when LLM::Function::Ractor::Task then :ractor
92
- else :task
58
+ def wait_tasks(tasks)
59
+ return [] if tasks.empty?
60
+ results = {}
61
+ grouped_tasks = tasks.group_by(&:group_class)
62
+ grouped_tasks.each do |group_class, group|
63
+ returns = group_class.new(group).wait
64
+ returns.each.with_index { results[group[_2]] = _1 }
93
65
  end
66
+ tasks.map { results[_1] }
94
67
  end
95
68
 
96
69
  def fire_hooks(tasks, results)
data/lib/llm/stream.rb CHANGED
@@ -46,11 +46,11 @@ module LLM
46
46
 
47
47
  ##
48
48
  # Waits for queued tool work to finish and returns function results.
49
- # @param [Symbol] strategy
50
- # The concurrency strategy to use
49
+ # Any passed arguments are ignored because queued work is waited according
50
+ # to the actual task types already present in the queue.
51
51
  # @return [Array<LLM::Function::Return>]
52
- def wait(strategy)
53
- queue.wait(strategy)
52
+ def wait(*)
53
+ queue.wait
54
54
  end
55
55
 
56
56
  # @group Public callbacks
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Transport
4
+ ##
5
+ # Internal request execution methods for {LLM::Provider}.
6
+ #
7
+ # This module handles provider-side transport execution, response
8
+ # parsing, streaming, and request body setup.
9
+ #
10
+ # @api private
11
+ module Execution
12
+ private
13
+
14
+ ##
15
+ # Executes a HTTP request
16
+ # @param [Net::HTTPRequest] request
17
+ # The request to send
18
+ # @param [Proc] b
19
+ # A block to yield the response to (optional)
20
+ # @return [LLM::Transport::Response]
21
+ # The response from the server
22
+ # @raise [LLM::Error::Unauthorized]
23
+ # When authentication fails
24
+ # @raise [LLM::Error::RateLimit]
25
+ # When the rate limit is exceeded
26
+ # @raise [LLM::Error]
27
+ # When any other unsuccessful status code is returned
28
+ # @raise [SystemCallError]
29
+ # When there is a network error at the operating system level
30
+ # @return [LLM::Transport::Response]
31
+ def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, inputs: nil, &b)
32
+ stream &&= LLM::Object.from(streamer: stream, parser: stream_parser, decoder: stream_decoder)
33
+ owner = transport.request_owner
34
+ tracer = self.tracer
35
+ span = tracer.on_request_start(operation:, model:, inputs:)
36
+ res = transport.request(request, owner:, stream:, &b)
37
+ res = LLM::Transport::Response.from(res)
38
+ [handle_response(res, tracer, span), span, tracer]
39
+ rescue *transport.interrupt_errors
40
+ raise LLM::Interrupt, "request interrupted" if transport.interrupted?(owner)
41
+ raise
42
+ end
43
+
44
+ ##
45
+ # Handles the response from a request
46
+ # @param [LLM::Transport::Response] res
47
+ # The response to handle
48
+ # @param [Object, nil] span
49
+ # The span
50
+ # @return [LLM::Transport::Response]
51
+ def handle_response(res, tracer, span)
52
+ res.ok? ? res.body = parse_response(res) : error_handler.new(tracer, span, res).raise_error!
53
+ res
54
+ end
55
+
56
+ ##
57
+ # Parse a HTTP response
58
+ # @param [LLM::Transport::Response] res
59
+ # @return [LLM::Object, String]
60
+ def parse_response(res)
61
+ case res["content-type"]
62
+ when %r{\Aapplication/json\s*} then LLM::Object.from(LLM.json.load(res.body))
63
+ else res.body
64
+ end
65
+ end
66
+ end
67
+ end