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
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ class LLM::Transport
6
+ ##
7
+ # The {LLM::Transport::HTTP LLM::Transport::HTTP} transport is the
8
+ # built-in adapter for Ruby's {Net::HTTP Net::HTTP}. It manages
9
+ # transient HTTP connections, tracks active requests by owner, and
10
+ # interrupts in-flight requests when needed.
11
+ #
12
+ # @api private
13
+ class HTTP < self
14
+ INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
15
+ Request = Struct.new(:client, keyword_init: true)
16
+
17
+ ##
18
+ # @param [String] host
19
+ # @param [Integer] port
20
+ # @param [Integer] timeout
21
+ # @param [Boolean] ssl
22
+ # @return [LLM::Transport::HTTP]
23
+ def initialize(host:, port:, timeout:, ssl:)
24
+ @host = host
25
+ @port = port
26
+ @timeout = timeout
27
+ @ssl = ssl
28
+ @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
29
+ @monitor = Monitor.new
30
+ end
31
+
32
+ ##
33
+ # Returns the current request owner.
34
+ # @return [Object]
35
+ def request_owner
36
+ return Fiber.current unless defined?(::Async)
37
+ Async::Task.current? ? Async::Task.current : Fiber.current
38
+ end
39
+
40
+ ##
41
+ # @return [Array<Class<Exception>>]
42
+ def interrupt_errors
43
+ [*INTERRUPT_ERRORS, *optional_interrupt_errors]
44
+ end
45
+
46
+ ##
47
+ # Interrupt an active request, if any.
48
+ # @param [Fiber] owner
49
+ # @return [nil]
50
+ def interrupt!(owner)
51
+ req = request_for(owner) or return
52
+ lock { (@interrupts ||= {})[owner] = true }
53
+ close_socket(req.client)
54
+ req.client.finish if req.client.active?
55
+ owner.stop if owner.respond_to?(:stop)
56
+ rescue *interrupt_errors
57
+ nil
58
+ end
59
+
60
+ ##
61
+ # Returns whether an execution owner was interrupted.
62
+ # @param [Fiber] owner
63
+ # @return [Boolean, nil]
64
+ def interrupted?(owner)
65
+ lock { @interrupts&.delete(owner) }
66
+ end
67
+
68
+ ##
69
+ # Performs a request on the current HTTP transport.
70
+ # @param [Net::HTTPRequest] request
71
+ # @param [Fiber] owner
72
+ # @param [LLM::Object, nil] stream
73
+ # @yieldparam [LLM::Transport::Response] response
74
+ # @return [Object]
75
+ def request(request, owner:, stream: nil, &b)
76
+ client = client()
77
+ set_request(Request.new(client:), owner)
78
+ perform_request(client, request, stream, &b)
79
+ ensure
80
+ clear_request(owner)
81
+ end
82
+
83
+ ##
84
+ # @return [String]
85
+ def inspect
86
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :host, :port, :timeout, :ssl, :base_uri
92
+
93
+ def client
94
+ client = Net::HTTP.new(host, port)
95
+ client.read_timeout = timeout
96
+ client.use_ssl = ssl
97
+ client
98
+ end
99
+
100
+ def close_socket(http)
101
+ socket = http&.instance_variable_get(:@socket) or return
102
+ socket = socket.io if socket.respond_to?(:io)
103
+ socket.close
104
+ rescue *interrupt_errors
105
+ nil
106
+ end
107
+
108
+ def request_for(owner)
109
+ lock do
110
+ @requests ||= {}
111
+ @requests[owner]
112
+ end
113
+ end
114
+
115
+ def set_request(req, owner)
116
+ lock do
117
+ @requests ||= {}
118
+ @requests[owner] = req
119
+ end
120
+ end
121
+
122
+ def clear_request(owner)
123
+ lock { @requests&.delete(owner) }
124
+ end
125
+
126
+ def lock(&)
127
+ @monitor.synchronize(&)
128
+ end
129
+
130
+ def optional_interrupt_errors
131
+ defined?(::Async::Stop) ? [Async::Stop] : []
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Transport
4
+ ##
5
+ # The {LLM::Transport::PersistentHTTP LLM::Transport::PersistentHTTP}
6
+ # transport is the built-in adapter for
7
+ # [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent).
8
+ # It manages pooled HTTP connections, tracks active requests by owner,
9
+ # and interrupts in-flight requests when needed.
10
+ #
11
+ # @api private
12
+ class PersistentHTTP < self
13
+ INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
14
+ Request = Struct.new(:client, :connection, keyword_init: true)
15
+ @registry = {}
16
+ @monitor = Monitor.new
17
+
18
+ ##
19
+ # Returns the process-wide connection pool registry.
20
+ # @return [Hash]
21
+ def self.registry
22
+ @registry
23
+ end
24
+
25
+ def self.lock(&)
26
+ @monitor.synchronize(&)
27
+ end
28
+
29
+ ##
30
+ # @param [String] host
31
+ # @param [Integer] port
32
+ # @param [Integer] timeout
33
+ # @param [Boolean] ssl
34
+ # @return [LLM::Transport::PersistentHTTP]
35
+ def initialize(host:, port:, timeout:, ssl:)
36
+ @host = host
37
+ @port = port
38
+ @timeout = timeout
39
+ @ssl = ssl
40
+ @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
41
+ @monitor = Monitor.new
42
+ end
43
+
44
+ ##
45
+ # Returns the current request owner.
46
+ # @return [Object]
47
+ def request_owner
48
+ return Fiber.current unless defined?(::Async)
49
+ Async::Task.current? ? Async::Task.current : Fiber.current
50
+ end
51
+
52
+ ##
53
+ # @return [Array<Class<Exception>>]
54
+ def interrupt_errors
55
+ [*INTERRUPT_ERRORS, *optional_interrupt_errors]
56
+ end
57
+
58
+ ##
59
+ # Interrupt an active request, if any.
60
+ # @param [Fiber] owner
61
+ # @return [nil]
62
+ def interrupt!(owner)
63
+ req = request_for(owner) or return
64
+ lock { (@interrupts ||= {})[owner] = true }
65
+ close_socket(req.connection&.http)
66
+ req.client.finish(req.connection)
67
+ owner.stop if owner.respond_to?(:stop)
68
+ rescue *interrupt_errors
69
+ nil
70
+ end
71
+
72
+ ##
73
+ # Returns whether an execution owner was interrupted.
74
+ # @param [Fiber] owner
75
+ # @return [Boolean, nil]
76
+ def interrupted?(owner)
77
+ lock { @interrupts&.delete(owner) }
78
+ end
79
+
80
+ ##
81
+ # Performs a request on the current HTTP transport.
82
+ # @param [Net::HTTPRequest] request
83
+ # @param [Fiber] owner
84
+ # @param [LLM::Object, nil] stream
85
+ # @yieldparam [LLM::Transport::Response] response
86
+ # @return [Object]
87
+ def request(request, owner:, stream: nil, &b)
88
+ client.connection_for(URI.join(base_uri, request.path)) do |connection|
89
+ set_request(Request.new(client:, connection:), owner)
90
+ perform_request(connection.http, request, stream, &b)
91
+ end
92
+ ensure
93
+ clear_request(owner)
94
+ end
95
+
96
+ private
97
+
98
+ attr_reader :host, :port, :timeout, :ssl, :base_uri
99
+
100
+ def client
101
+ self.class.lock do
102
+ if self.class.registry[client_id]
103
+ self.class.registry[client_id]
104
+ else
105
+ LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
106
+ client = Net::HTTP::Persistent.new(name: self.class.name)
107
+ client.read_timeout = timeout
108
+ client.open_timeout = timeout
109
+ self.class.registry[client_id] = client
110
+ end
111
+ end
112
+ end
113
+
114
+ def client_id
115
+ "#{host}:#{port}:#{timeout}:#{ssl}"
116
+ end
117
+
118
+ def close_socket(http)
119
+ socket = http&.instance_variable_get(:@socket) or return
120
+ socket = socket.io if socket.respond_to?(:io)
121
+ socket.close
122
+ rescue *interrupt_errors
123
+ nil
124
+ end
125
+
126
+ def request_for(owner)
127
+ lock do
128
+ @requests ||= {}
129
+ @requests[owner]
130
+ end
131
+ end
132
+
133
+ def set_request(req, owner)
134
+ lock do
135
+ @requests ||= {}
136
+ @requests[owner] = req
137
+ end
138
+ end
139
+
140
+ def clear_request(owner)
141
+ lock { @requests&.delete(owner) }
142
+ end
143
+
144
+ def lock(&)
145
+ @monitor.synchronize(&)
146
+ end
147
+
148
+ def optional_interrupt_errors
149
+ defined?(::Async::Stop) ? [Async::Stop] : []
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Transport::Response
4
+ ##
5
+ # {LLM::Transport::Response::HTTP LLM::Transport::Response::HTTP}
6
+ # adapts a {Net::HTTPResponse Net::HTTPResponse} to the
7
+ # {LLM::Transport::Response LLM::Transport::Response} interface.
8
+ #
9
+ # This is the default wrapper for responses produced by the built-in
10
+ # {LLM::Transport::HTTP LLM::Transport::HTTP} transport.
11
+ class HTTP < self
12
+ ##
13
+ # @return [Net::HTTPResponse]
14
+ attr_reader :res
15
+
16
+ ##
17
+ # @param [Net::HTTPResponse] res
18
+ # @return [LLM::Transport::Response::HTTP]
19
+ def initialize(res)
20
+ @res = res
21
+ end
22
+
23
+ ##
24
+ # @return [String]
25
+ def code
26
+ @res.code
27
+ end
28
+
29
+ ##
30
+ # @return [Object]
31
+ def body
32
+ @res.body
33
+ end
34
+
35
+ ##
36
+ # @param [Object] value
37
+ # @return [Object]
38
+ def body=(value)
39
+ @res.body = value
40
+ end
41
+
42
+ ##
43
+ # @param [String] key
44
+ # @return [String, nil]
45
+ def [](key)
46
+ @res[key]
47
+ end
48
+
49
+ ##
50
+ # @param [Object, nil] dest
51
+ # @yieldparam [String] chunk
52
+ # @return [void]
53
+ def read_body(dest = nil, &block)
54
+ if dest && block
55
+ @res.read_body(dest) { block.call(_1) }
56
+ elsif dest
57
+ @res.read_body(dest)
58
+ elsif block
59
+ @res.read_body { block.call(_1) }
60
+ else
61
+ @res.read_body
62
+ end
63
+ end
64
+
65
+ ##
66
+ # @return [Boolean]
67
+ def success?
68
+ Net::HTTPSuccess === @res
69
+ end
70
+
71
+ ##
72
+ # @return [Boolean]
73
+ def ok?
74
+ Net::HTTPOK === @res
75
+ end
76
+
77
+ ##
78
+ # @return [Boolean]
79
+ def bad_request?
80
+ Net::HTTPBadRequest === @res
81
+ end
82
+
83
+ ##
84
+ # @return [Boolean]
85
+ def unauthorized?
86
+ Net::HTTPUnauthorized === @res
87
+ end
88
+
89
+ ##
90
+ # @return [Boolean]
91
+ def forbidden?
92
+ Net::HTTPForbidden === @res
93
+ end
94
+
95
+ ##
96
+ # @return [Boolean]
97
+ def not_found?
98
+ Net::HTTPNotFound === @res
99
+ end
100
+
101
+ ##
102
+ # @return [Boolean]
103
+ def rate_limited?
104
+ Net::HTTPTooManyRequests === @res
105
+ end
106
+
107
+ ##
108
+ # @return [Boolean]
109
+ def server_error?
110
+ Net::HTTPServerError === @res
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Transport
4
+ ##
5
+ # {LLM::Transport::Response LLM::Transport::Response} defines the
6
+ # normalized HTTP response interface expected by transports and
7
+ # provider error handlers.
8
+ #
9
+ # Custom transports can execute requests through any underlying HTTP
10
+ # client, then adapt that client's native response object to this
11
+ # interface.
12
+ #
13
+ # This keeps the transport boundary focused on one contract:
14
+ # providers, execution, and error handlers only need a response
15
+ # object that implements
16
+ # {LLM::Transport::Response LLM::Transport::Response}, regardless of
17
+ # how the request was actually performed.
18
+ class Response
19
+ require_relative "response/http"
20
+
21
+ ##
22
+ # @param [Object] res
23
+ # @return [LLM::Transport::Response]
24
+ def self.from(res)
25
+ return res if LLM::Transport::Response === res
26
+ return HTTP.new(res) if Net::HTTPResponse === res
27
+ res
28
+ end
29
+
30
+ ##
31
+ # @return [String]
32
+ def code
33
+ raise NotImplementedError
34
+ end
35
+
36
+ ##
37
+ # @return [Object]
38
+ def body
39
+ raise NotImplementedError
40
+ end
41
+
42
+ ##
43
+ # @param [Object] value
44
+ # @return [Object]
45
+ def body=(value)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ ##
50
+ # @param [String] key
51
+ # @return [String, nil]
52
+ def [](key)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ ##
57
+ # @param [Object, nil] dest
58
+ # @yieldparam [String] chunk
59
+ # @return [void]
60
+ def read_body(dest = nil, &)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ ##
65
+ # @return [Boolean]
66
+ def success?
67
+ raise NotImplementedError
68
+ end
69
+
70
+ ##
71
+ # @return [Boolean]
72
+ def ok?
73
+ raise NotImplementedError
74
+ end
75
+
76
+ ##
77
+ # @return [Boolean]
78
+ def bad_request?
79
+ raise NotImplementedError
80
+ end
81
+
82
+ ##
83
+ # @return [Boolean]
84
+ def unauthorized?
85
+ raise NotImplementedError
86
+ end
87
+
88
+ ##
89
+ # @return [Boolean]
90
+ def forbidden?
91
+ raise NotImplementedError
92
+ end
93
+
94
+ ##
95
+ # @return [Boolean]
96
+ def not_found?
97
+ raise NotImplementedError
98
+ end
99
+
100
+ ##
101
+ # @return [Boolean]
102
+ def rate_limited?
103
+ raise NotImplementedError
104
+ end
105
+
106
+ ##
107
+ # @return [Boolean]
108
+ def server_error?
109
+ raise NotImplementedError
110
+ end
111
+ end
112
+ end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Provider::Transport
3
+ class LLM::Transport
4
4
  ##
5
- # @private
6
- class HTTP::StreamDecoder
5
+ # {LLM::Transport::StreamDecoder LLM::Transport::StreamDecoder}
6
+ # incrementally decodes streamed HTTP response bodies into parser
7
+ # events.
8
+ #
9
+ # @api private
10
+ class StreamDecoder
7
11
  ##
8
12
  # @return [Object]
9
13
  attr_reader :parser
10
14
 
11
15
  ##
12
16
  # @param [#parse!, #body] parser
13
- # @return [LLM::Provider::Transport::HTTP::StreamDecoder]
17
+ # @return [LLM::Transport::StreamDecoder]
14
18
  def initialize(parser)
15
19
  @buffer = +""
16
20
  @cursor = 0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # The {LLM::Transport LLM::Transport} class defines the execution
6
+ # interface used by {LLM::Provider}.
7
+ #
8
+ # Custom transports can subclass this class and override {#request} to
9
+ # execute provider requests without changing request adapters or
10
+ # response adapters.
11
+ #
12
+ # Providers currently construct {Net::HTTPRequest Net::HTTPRequest}
13
+ # objects before delegating to a transport. Custom transports are
14
+ # therefore expected to execute those requests directly, or transform
15
+ # them into backend-specific request objects before execution.
16
+ #
17
+ # Only {#request} is required. The remaining methods are optional hooks
18
+ # for features such as interruption, request ownership, or persistence,
19
+ # and only need to be implemented when the underlying adapter can
20
+ # support them.
21
+ #
22
+ # Returned responses should implement the
23
+ # {LLM::Transport::Response LLM::Transport::Response} interface. In
24
+ # practice this can mean adapting another client's response object so
25
+ # existing provider execution, response adapters, and error handlers
26
+ # can rely on one normalized response contract instead of
27
+ # transport-specific classes.
28
+ class Transport
29
+ require_relative "transport/response"
30
+ require_relative "transport/stream_decoder"
31
+ require_relative "transport/http"
32
+ require_relative "transport/persistent_http"
33
+ require_relative "transport/execution"
34
+
35
+ ##
36
+ # Returns the built-in Net::HTTP transport class.
37
+ # @return [Class]
38
+ def self.net_http
39
+ HTTP
40
+ end
41
+
42
+ ##
43
+ # Returns the built-in Net::HTTP::Persistent transport class.
44
+ # @return [Class]
45
+ def self.net_http_persistent
46
+ PersistentHTTP
47
+ end
48
+
49
+ ##
50
+ # Performs a request through the transport.
51
+ # @param [Net::HTTPRequest] request
52
+ # @param [Object] owner
53
+ # @param [LLM::Object, nil] stream
54
+ # @yieldparam [LLM::Transport::Response] response
55
+ # @return [Object]
56
+ def request(request, owner:, stream: nil, &)
57
+ raise NotImplementedError
58
+ end
59
+
60
+ ##
61
+ # Returns the current request owner.
62
+ # @return [Object]
63
+ def request_owner
64
+ return Fiber.current unless defined?(::Async)
65
+ Async::Task.current? ? Async::Task.current : Fiber.current
66
+ end
67
+
68
+ ##
69
+ # Returns the exception classes that indicate an interrupted request.
70
+ # @return [Array<Class<Exception>>]
71
+ def interrupt_errors
72
+ []
73
+ end
74
+
75
+ ##
76
+ # Interrupt an active request, if any.
77
+ # @param [Object] owner
78
+ # @return [nil]
79
+ def interrupt!(owner)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ ##
84
+ # Returns whether an execution owner was interrupted.
85
+ # @param [Object] owner
86
+ # @return [Boolean, nil]
87
+ def interrupted?(owner)
88
+ nil
89
+ end
90
+
91
+ ##
92
+ # @note
93
+ # Custom transports may be able to reuse this helper when they
94
+ # operate on Net::HTTPRequest objects, or implement their own
95
+ # request body preparation path instead.
96
+ # @param [Net::HTTPRequest] request
97
+ # @param [IO] io
98
+ # @return [void]
99
+ def set_body_stream(request, io)
100
+ request.body_stream = io
101
+ request["transfer-encoding"] = "chunked" unless request["content-length"]
102
+ end
103
+
104
+ private
105
+
106
+ ##
107
+ # @api private
108
+ # @note
109
+ # Custom transports may be able to reuse this helper when they
110
+ # execute requests through a Net::HTTP-compatible client, or
111
+ # implement their own request execution path instead.
112
+ def perform_request(client, request, stream, &b)
113
+ if stream
114
+ client.request(request) do |raw|
115
+ res = LLM::Transport::Response.from(raw)
116
+ if res.success?
117
+ parser = stream.decoder.new(stream.parser.new(stream.streamer))
118
+ res.read_body(parser)
119
+ body = parser.body
120
+ res.body = (Hash === body || Array === body) ? LLM::Object.from(body) : body
121
+ else
122
+ body = +""
123
+ res.read_body { body << _1 }
124
+ res.body = body
125
+ end
126
+ ensure
127
+ parser&.free
128
+ end
129
+ elsif b
130
+ client.request(request) do |raw|
131
+ res = LLM::Transport::Response.from(raw)
132
+ res.success? ? b.call(res) : res
133
+ end
134
+ else
135
+ LLM::Transport::Response.from(client.request(request))
136
+ end
137
+ end
138
+ end
139
+ end
data/lib/llm/usage.rb CHANGED
@@ -4,13 +4,22 @@
4
4
  # The {LLM::Usage LLM::Usage} class represents token usage for
5
5
  # a given conversation or completion. As a conversation grows,
6
6
  # so does the number of tokens used. This class helps track
7
- # the number of input, output, reasoning and overall token count.
8
- # It can also help track usage of the context window (which may
9
- # vary by model).
10
- class LLM::Usage < Struct.new(:input_tokens, :output_tokens, :reasoning_tokens, :total_tokens, keyword_init: true)
7
+ # the number of input, output, reasoning, cache, and overall
8
+ # token count. It can also help track usage of the context
9
+ # window (which may vary by model).
10
+ class LLM::Usage < Struct.new(
11
+ :input_tokens, :output_tokens, :reasoning_tokens,
12
+ :input_audio_tokens, :output_audio_tokens, :input_image_tokens,
13
+ :cache_read_tokens, :cache_write_tokens, :total_tokens, keyword_init: true
14
+ )
11
15
  ##
12
16
  # @return [String]
13
17
  def to_json(...)
14
- LLM.json.dump({input_tokens:, output_tokens:, reasoning_tokens:, total_tokens:})
18
+ LLM.json.dump({
19
+ input_tokens:, output_tokens:,
20
+ reasoning_tokens:,
21
+ input_audio_tokens:, output_audio_tokens:, input_image_tokens:,
22
+ cache_read_tokens:, cache_write_tokens:, total_tokens:
23
+ })
15
24
  end
16
25
  end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "8.0.0"
4
+ VERSION = "9.0.0"
5
5
  end