ruby_llm-bedrock_invoke 0.1.0.pre.1

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.
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module BedrockInvoke
5
+ # The Anthropic Messages wire format over Bedrock InvokeModel (RubyLLM 2.0).
6
+ #
7
+ # Mirrors the in-tree VertexAI::Anthropic pattern: inherit the entire
8
+ # Anthropic protocol and override only the endpoints, the Bedrock body
9
+ # envelope (no top-level model/stream, anthropic_version marker), SigV4
10
+ # signing, and AWS event-stream decoding for streaming.
11
+ class Protocol < RubyLLM::Protocols::Anthropic
12
+ include EventStream
13
+
14
+ ANTHROPIC_VERSION = 'bedrock-2023-05-31'
15
+
16
+ def completion_url
17
+ "/model/#{RubyLLM::BedrockInvoke.escape_model_id(@model.id)}/invoke"
18
+ end
19
+
20
+ def stream_url
21
+ "/model/#{RubyLLM::BedrockInvoke.escape_model_id(@model.id)}/invoke-with-response-stream"
22
+ end
23
+
24
+ def render_payload(messages, **options)
25
+ payload = super
26
+ payload.delete(:model)
27
+ payload.delete(:stream)
28
+ payload[:anthropic_version] = ANTHROPIC_VERSION
29
+ ToolSearch.apply!(payload, options[:tools] || {})
30
+ payload
31
+ end
32
+
33
+ def parse_completion_body(data, raw:)
34
+ message = super
35
+ preserve_server_blocks(message, data)
36
+ message
37
+ end
38
+
39
+ # Replays messages that carried server-managed tool-search blocks
40
+ # verbatim; everything else renders through the stock Anthropic format.
41
+ def format_message(msg, **)
42
+ blocks = RubyLLM::BedrockInvoke.raw_blocks(msg)
43
+ return super unless blocks
44
+
45
+ { role: 'assistant', content: blocks }
46
+ end
47
+
48
+ # 2.0 gates thinking on registry reasoning_options, which assumed models
49
+ # never carry; Bedrock Claude supports budget thinking, so emit it
50
+ # directly. Adaptive/effort-only thinking is not available on Bedrock.
51
+ def build_thinking_payload(thinking, _model)
52
+ inner = RubyLLM::BedrockInvoke.budget_thinking_payload(thinking)
53
+ return nil if inner.nil?
54
+
55
+ { thinking: inner }
56
+ end
57
+
58
+ # Bedrock has no Anthropic Files API.
59
+ def supports_provider_file_references?
60
+ false
61
+ end
62
+
63
+ private
64
+
65
+ def sync_response(payload, additional_headers = {})
66
+ ToolSearch.ensure_beta!(payload)
67
+ body = JSON.generate(payload)
68
+ response = @connection.post(completion_url, body) do |req|
69
+ req.headers['Content-Type'] = 'application/json'
70
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
71
+ req.headers.merge!(@provider.sign_headers('POST', completion_url, body))
72
+ end
73
+ parse_completion_response(response)
74
+ end
75
+
76
+ def stream_response(payload, additional_headers = {}, &block)
77
+ ToolSearch.ensure_beta!(payload)
78
+ state = StreamState.new(self)
79
+ failure_buffer = +''
80
+ body = JSON.generate(payload)
81
+
82
+ response = @connection.post(stream_url, body) do |req|
83
+ req.headers['Content-Type'] = 'application/json'
84
+ req.headers['Accept'] = 'application/vnd.amazon.eventstream'
85
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
86
+ req.headers.merge!(@provider.sign_headers('POST', stream_url, body))
87
+ install_stream_handler(req) do |bytes, env, received|
88
+ consume_stream_bytes(bytes, env, received, state, failure_buffer, &block)
89
+ end
90
+ end
91
+
92
+ message = state.accumulator.to_message(response)
93
+ attach_collected_blocks(message, state.collector)
94
+ RubyLLM.logger.debug { "Stream completed: #{message.content}" }
95
+ message
96
+ end
97
+
98
+ def install_stream_handler(req, &handler)
99
+ if Faraday::VERSION.start_with?('1')
100
+ req.options[:on_data] = proc { |chunk, size| handler.call(chunk, nil, size) }
101
+ else
102
+ req.options.on_data = proc { |chunk, received, env| handler.call(chunk, env, received) }
103
+ end
104
+ end
105
+
106
+ def consume_stream_bytes(bytes, env, received, state, failure_buffer, &block)
107
+ return handle_failed_stream_chunk(bytes, env, failure_buffer) if failed_stream_chunk?(bytes, env)
108
+
109
+ state.reset_if_new_attempt!(bytes, received)
110
+ each_event_stream_event(state.decoder, bytes) do |event|
111
+ state.collector.observe(event)
112
+ next if state.collector.suppress?(event)
113
+
114
+ chunk = build_chunk(event)
115
+ next unless chunk
116
+
117
+ state.accumulator.add(chunk)
118
+ block.call(chunk)
119
+ end
120
+ end
121
+
122
+ # Faraday 1 gives on_data no response env, so non-200 JSON error bodies
123
+ # would otherwise be fed to the event-stream decoder (binary frames
124
+ # never start with '{' — the first bytes are a frame length prelude).
125
+ def failed_stream_chunk?(bytes, env)
126
+ return env.status != 200 if env
127
+
128
+ bytes.to_s.lstrip.start_with?('{')
129
+ end
130
+
131
+ # Error bodies can arrive split across reads; accumulate until they parse.
132
+ def handle_failed_stream_chunk(bytes, env, failure_buffer)
133
+ failure_buffer << bytes
134
+ data = JSON.parse(failure_buffer)
135
+ response = env ? env.merge(body: data) : Struct.new(:body, :status).new(data, 500)
136
+ ErrorMiddleware.parse_error(provider: @provider, response: response)
137
+ rescue JSON::ParserError
138
+ RubyLLM.logger.debug { "bedrock_invoke: accumulating failed stream chunk: #{bytes.inspect}" }
139
+ end
140
+
141
+ def error_middleware_provider
142
+ @provider
143
+ end
144
+
145
+ # Responses carrying server-managed tool-search blocks must be replayed
146
+ # verbatim. 2.0 Message content is String-only, so the block array rides
147
+ # in an ivar that format_message re-injects when rendering history.
148
+ def preserve_server_blocks(message, data)
149
+ blocks = data.is_a?(Hash) ? data['content'] : nil
150
+ return unless blocks && ToolSearch.contains_server_blocks?(blocks)
151
+
152
+ message.instance_variable_set(RubyLLM::BedrockInvoke::RAW_BLOCKS_IVAR, blocks)
153
+ end
154
+
155
+ def attach_collected_blocks(message, collector)
156
+ return unless collector.server_blocks?
157
+
158
+ message.instance_variable_set(RubyLLM::BedrockInvoke::RAW_BLOCKS_IVAR, collector.content_blocks)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Anthropic Claude on AWS Bedrock via the InvokeModel API (RubyLLM 1.x).
6
+ #
7
+ # Inherits the entire Anthropic Messages wire format — message/tool/schema
8
+ # rendering, structured output via output_config, cache_control, response
9
+ # and chunk parsing — and swaps the transport underneath: per-request
10
+ # SigV4 signing against bedrock-runtime and AWS event-stream decoding for
11
+ # InvokeModelWithResponseStream.
12
+ class BedrockInvoke < Anthropic
13
+ include RubyLLM::BedrockInvoke::Signing
14
+ include RubyLLM::BedrockInvoke::EventStream
15
+
16
+ ANTHROPIC_VERSION = 'bedrock-2023-05-31'
17
+
18
+ def api_base
19
+ config_value(:bedrock_invoke_api_base) ||
20
+ "https://bedrock-runtime.#{bedrock_invoke_region}.amazonaws.com"
21
+ end
22
+
23
+ def headers
24
+ {}
25
+ end
26
+
27
+ def completion_url
28
+ "/model/#{RubyLLM::BedrockInvoke.escape_model_id(@model_id)}/invoke"
29
+ end
30
+
31
+ def stream_url
32
+ "/model/#{RubyLLM::BedrockInvoke.escape_model_id(@model_id)}/invoke-with-response-stream"
33
+ end
34
+
35
+ # rubocop:disable Metrics/ParameterLists
36
+ def render_payload(messages, tools:, temperature:, model:, stream: false,
37
+ schema: nil, thinking: nil, tool_prefs: nil)
38
+ @model_id = model.id
39
+ payload = super
40
+ payload.delete(:model)
41
+ payload.delete(:stream)
42
+ payload[:anthropic_version] = ANTHROPIC_VERSION
43
+ RubyLLM::BedrockInvoke::ToolSearch.apply!(payload, tools)
44
+ payload
45
+ end
46
+ # rubocop:enable Metrics/ParameterLists
47
+
48
+ def parse_completion_response(response)
49
+ message = super
50
+ preserve_server_blocks(message, response)
51
+ message
52
+ end
53
+
54
+ def parse_error(response)
55
+ return if response.body.nil? || response.body.empty?
56
+
57
+ body = try_parse_json(response.body)
58
+ return body if body.is_a?(String)
59
+
60
+ body['message'] || body['Message'] || body['error'] || body['__type'] || super
61
+ end
62
+
63
+ def list_models
64
+ []
65
+ end
66
+
67
+ # Messages preserving server tool-search blocks replay verbatim.
68
+ # Bypassing the stock formatter also avoids its thinking-block prepend,
69
+ # which would both duplicate thinking blocks (they are already in the
70
+ # raw array) and permanently mutate the preserved array via unshift.
71
+ def format_message(msg, **)
72
+ blocks = RubyLLM::BedrockInvoke.raw_blocks(msg)
73
+ return { role: 'assistant', content: blocks } if blocks
74
+
75
+ super
76
+ end
77
+
78
+ # 1.16 gates thinking on registry reasoning_options, which assumed
79
+ # models never carry; Bedrock Claude supports budget thinking, so emit
80
+ # it directly. Called as (thinking) on 1.13-1.15 (bare payload spliced
81
+ # into payload[:thinking]) and as (thinking, model) on 1.16 (wrapped
82
+ # shape unwrapped by add_thinking_fields).
83
+ def build_thinking_payload(thinking, model = nil)
84
+ inner = RubyLLM::BedrockInvoke.budget_thinking_payload(thinking)
85
+ return nil if inner.nil?
86
+
87
+ model.nil? ? inner : { thinking: inner }
88
+ end
89
+
90
+ class << self
91
+ def slug
92
+ 'bedrock_invoke'
93
+ end
94
+
95
+ def capabilities
96
+ RubyLLM::BedrockInvoke::Capabilities
97
+ end
98
+
99
+ def configuration_requirements
100
+ []
101
+ end
102
+
103
+ def configuration_options
104
+ RubyLLM::BedrockInvoke::CONFIGURATION_OPTIONS
105
+ end
106
+
107
+ def configured?(config)
108
+ region = (config.bedrock_invoke_region if config.respond_to?(:bedrock_invoke_region))
109
+ !(region || ENV['AWS_REGION'] || ENV.fetch('AWS_DEFAULT_REGION', nil)).nil?
110
+ end
111
+
112
+ def assume_models_exist?
113
+ true
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def sync_response(connection, payload, additional_headers = {})
120
+ RubyLLM::BedrockInvoke::ToolSearch.ensure_beta!(payload)
121
+ body = JSON.generate(payload)
122
+ response = connection.post(completion_url, body) do |req|
123
+ req.headers['Content-Type'] = 'application/json'
124
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
125
+ req.headers.merge!(sign_headers('POST', completion_url, body))
126
+ end
127
+ parse_completion_response(response)
128
+ end
129
+
130
+ def stream_response(connection, payload, additional_headers = {}, &block)
131
+ RubyLLM::BedrockInvoke::ToolSearch.ensure_beta!(payload)
132
+ state = RubyLLM::BedrockInvoke::StreamState.new(self)
133
+ failure_buffer = +''
134
+ body = JSON.generate(payload)
135
+
136
+ response = connection.post(stream_url, body) do |req|
137
+ req.headers['Content-Type'] = 'application/json'
138
+ req.headers['Accept'] = 'application/vnd.amazon.eventstream'
139
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
140
+ req.headers.merge!(sign_headers('POST', stream_url, body))
141
+ install_stream_handler(req) do |bytes, env, received|
142
+ consume_stream_bytes(bytes, env, received, state, failure_buffer, &block)
143
+ end
144
+ end
145
+
146
+ message = state.accumulator.to_message(response)
147
+ attach_collected_blocks(message, state.collector)
148
+ RubyLLM.logger.debug { "Stream completed: #{message.content}" }
149
+ message
150
+ end
151
+
152
+ def install_stream_handler(req, &handler)
153
+ if Faraday::VERSION.start_with?('1')
154
+ req.options[:on_data] = proc { |chunk, size| handler.call(chunk, nil, size) }
155
+ else
156
+ req.options.on_data = proc { |chunk, received, env| handler.call(chunk, env, received) }
157
+ end
158
+ end
159
+
160
+ def consume_stream_bytes(bytes, env, received, state, failure_buffer, &block)
161
+ return handle_failed_stream_chunk(bytes, env, failure_buffer) if failed_stream_chunk?(bytes, env)
162
+
163
+ state.reset_if_new_attempt!(bytes, received)
164
+ each_event_stream_event(state.decoder, bytes) do |event|
165
+ state.collector.observe(event)
166
+ next if state.collector.suppress?(event)
167
+
168
+ chunk = build_chunk(event)
169
+ next unless chunk
170
+
171
+ state.accumulator.add(chunk)
172
+ block.call(chunk)
173
+ end
174
+ end
175
+
176
+ # Faraday 1 gives on_data no response env, so non-200 JSON error bodies
177
+ # would otherwise be fed to the event-stream decoder (binary frames
178
+ # never start with '{' — the first bytes are a frame length prelude).
179
+ def failed_stream_chunk?(bytes, env)
180
+ return env.status != 200 if env
181
+
182
+ bytes.to_s.lstrip.start_with?('{')
183
+ end
184
+
185
+ # Error bodies can arrive split across reads; accumulate until they parse.
186
+ def handle_failed_stream_chunk(bytes, env, failure_buffer)
187
+ failure_buffer << bytes
188
+ data = JSON.parse(failure_buffer)
189
+ response = env ? env.merge(body: data) : Struct.new(:body, :status).new(data, 500)
190
+ ErrorMiddleware.parse_error(provider: self, response: response)
191
+ rescue JSON::ParserError
192
+ RubyLLM.logger.debug { "bedrock_invoke: accumulating failed stream chunk: #{bytes.inspect}" }
193
+ end
194
+
195
+ # When the response carries server-managed tool-search blocks, the whole
196
+ # content-block array must go back to the API verbatim on the next
197
+ # request. Message exposes no writer, so the content ivar is replaced
198
+ # in place — the safe cross-version seam given 1.13-1.16 constructor drift.
199
+ def preserve_server_blocks(message, response)
200
+ blocks = response.body.is_a?(Hash) ? response.body['content'] : nil
201
+ return unless blocks && RubyLLM::BedrockInvoke::ToolSearch.contains_server_blocks?(blocks)
202
+
203
+ raw = RubyLLM::BedrockInvoke::RawContent.new(blocks, text: message.content)
204
+ message.instance_variable_set(:@content, raw)
205
+ end
206
+
207
+ def attach_collected_blocks(message, collector)
208
+ return unless collector.server_blocks?
209
+
210
+ raw = RubyLLM::BedrockInvoke::RawContent.new(collector.content_blocks, text: message.content)
211
+ message.instance_variable_set(:@content, raw)
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Anthropic Claude on AWS Bedrock via the InvokeModel API (RubyLLM 2.0).
6
+ #
7
+ # Where/auth only — the wire format lives in
8
+ # RubyLLM::BedrockInvoke::Protocol. Credentials come from the standard
9
+ # AWS chain (env vars, shared config, IRSA/Pod Identity, IMDS) unless a
10
+ # credential provider is configured explicitly.
11
+ class BedrockInvoke < Provider
12
+ include RubyLLM::BedrockInvoke::Signing
13
+
14
+ protocol :anthropic, RubyLLM::BedrockInvoke::Protocol
15
+
16
+ def api_base
17
+ config_value(:bedrock_invoke_api_base) ||
18
+ "https://bedrock-runtime.#{bedrock_invoke_region}.amazonaws.com"
19
+ end
20
+
21
+ def headers
22
+ {}
23
+ end
24
+
25
+ def ensure_configured!
26
+ return if self.class.configured?(@config)
27
+
28
+ raise ConfigurationError,
29
+ 'No AWS region configured for bedrock_invoke. Set config.bedrock_invoke_region ' \
30
+ 'or the AWS_REGION environment variable.'
31
+ end
32
+
33
+ def parse_error(response)
34
+ body = response.body
35
+ body = try_parse_json(body) if respond_to?(:try_parse_json, true)
36
+ return if body.nil? || (body.respond_to?(:empty?) && body.empty?)
37
+
38
+ return body if body.is_a?(String)
39
+
40
+ body['message'] || body['Message'] || body['error'] || body['__type'] || super
41
+ end
42
+
43
+ def list_models
44
+ []
45
+ end
46
+
47
+ class << self
48
+ def capabilities
49
+ RubyLLM::BedrockInvoke::Capabilities
50
+ end
51
+
52
+ def configuration_options
53
+ RubyLLM::BedrockInvoke::CONFIGURATION_OPTIONS
54
+ end
55
+
56
+ def configuration_requirements
57
+ []
58
+ end
59
+
60
+ def configured?(config)
61
+ region = (config.bedrock_invoke_region if config.respond_to?(:bedrock_invoke_region))
62
+ !(region || ENV['AWS_REGION'] || ENV.fetch('AWS_DEFAULT_REGION', nil)).nil?
63
+ end
64
+
65
+ def assume_models_exist?
66
+ true
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module BedrockInvoke
5
+ # Raw Anthropic content blocks that must be replayed verbatim on
6
+ # subsequent requests (server_tool_use / tool_search_tool_result and their
7
+ # siblings), while still displaying as the assistant's text.
8
+ #
9
+ # RubyLLM renders RubyLLM::Content::Raw message content into the payload
10
+ # unchanged, which is exactly what the tool search tool requires: its
11
+ # server-side blocks are passed back untouched and expanded by the API.
12
+ class RawContent < RubyLLM::Content::Raw
13
+ def initialize(value, text: nil)
14
+ super(value)
15
+ @text = text
16
+ end
17
+
18
+ def to_s
19
+ @text.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-core'
4
+ require 'uri'
5
+
6
+ module RubyLLM
7
+ module BedrockInvoke
8
+ # SigV4 request signing backed by the standard AWS credential chain.
9
+ #
10
+ # Credentials resolve exactly like any AWS SDK client: environment
11
+ # variables, shared config/credentials files, SSO, ECS/EKS container
12
+ # credentials (Pod Identity), IRSA web-identity tokens, and IMDS. A
13
+ # token projected into a pod therefore "just works" with no RubyLLM
14
+ # configuration beyond the region.
15
+ #
16
+ # Mixed into objects that expose +@config+ (the RubyLLM configuration)
17
+ # and +api_base+.
18
+ module Signing
19
+ SERVICE = 'bedrock'
20
+
21
+ # The default chain is resolved once per process per region and shared:
22
+ # the returned credentials object self-refreshes (the AWS SDK norm),
23
+ # and re-resolving per Chat would mean an STS/IMDS network round trip
24
+ # for every RubyLLM.chat under IRSA or instance profiles.
25
+ DEFAULT_CHAIN_MUTEX = Mutex.new
26
+
27
+ class << self
28
+ def cached_default_chain_credentials(region)
29
+ DEFAULT_CHAIN_MUTEX.synchronize do
30
+ @default_chain_cache ||= {}
31
+ @default_chain_cache[region] ||= ::Aws::STS::Client.new(region: region).config.credentials
32
+ end
33
+ end
34
+
35
+ # For tests and credential rotation.
36
+ def reset_default_chain_cache!
37
+ DEFAULT_CHAIN_MUTEX.synchronize { @default_chain_cache = {} }
38
+ end
39
+ end
40
+
41
+ def bedrock_invoke_region
42
+ region = presence(config_value(:bedrock_invoke_region)) ||
43
+ presence(ENV.fetch('AWS_REGION', nil)) ||
44
+ presence(ENV.fetch('AWS_DEFAULT_REGION', nil))
45
+ return region if region
46
+
47
+ raise RubyLLM::ConfigurationError,
48
+ 'No AWS region configured for bedrock_invoke. Set config.bedrock_invoke_region ' \
49
+ 'or the AWS_REGION environment variable.'
50
+ end
51
+
52
+ def sigv4_signer
53
+ @sigv4_signer ||= ::Aws::Sigv4::Signer.new(
54
+ service: SERVICE,
55
+ region: bedrock_invoke_region,
56
+ credentials_provider: bedrock_invoke_credentials
57
+ )
58
+ end
59
+
60
+ # Returns headers (authorization, x-amz-date, x-amz-content-sha256 and,
61
+ # for temporary credentials, x-amz-security-token) for one request.
62
+ # +path+ is relative to +api_base+; +body+ must be the exact bytes sent.
63
+ # URI.join mirrors Faraday's absolute-path semantics, so a custom
64
+ # api_base carrying a path component cannot desynchronize the signature
65
+ # from the transmitted URL.
66
+ def sign_headers(method, path, body)
67
+ signature = sigv4_signer.sign_request(
68
+ http_method: method,
69
+ url: URI.join(api_base, path).to_s,
70
+ body: body
71
+ )
72
+ signature.headers
73
+ end
74
+
75
+ def bedrock_invoke_credentials
76
+ explicit = config_value(:bedrock_invoke_credential_provider)
77
+ return explicit if explicit
78
+
79
+ credentials = Signing.cached_default_chain_credentials(bedrock_invoke_region)
80
+ return credentials if credentials
81
+
82
+ raise RubyLLM::ConfigurationError,
83
+ 'No AWS credentials found for bedrock_invoke. Provide credentials via the standard ' \
84
+ 'AWS chain (env vars, shared config, IRSA/Pod Identity, instance profile) or set ' \
85
+ 'config.bedrock_invoke_credential_provider.'
86
+ end
87
+
88
+ private
89
+
90
+ def config_value(key)
91
+ return nil unless @config.respond_to?(key)
92
+
93
+ @config.public_send(key)
94
+ end
95
+
96
+ # On <= 1.15 the config accessors are plain attr_accessors, so empty
97
+ # strings are not coerced to nil the way 1.16+/2.0 writers do.
98
+ def presence(value)
99
+ return nil if value.nil?
100
+ return nil if value.respond_to?(:empty?) && value.to_s.strip.empty?
101
+
102
+ value
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module BedrockInvoke
5
+ # Per-attempt streaming state: accumulator, block collector, and
6
+ # event-stream decoder.
7
+ #
8
+ # RubyLLM's Faraday connection installs retry middleware that re-issues
9
+ # POSTs on retryable failures (throttling raised mid-stream, connection
10
+ # drops). The on_data callback closes over this holder; resetting it at
11
+ # the first chunk of each physical attempt prevents a retried stream from
12
+ # appending to a half-filled accumulator or feeding a decoder stuck on a
13
+ # partial frame. Detection: Faraday reports cumulative received bytes,
14
+ # which equal the chunk size exactly when an attempt starts fresh.
15
+ class StreamState
16
+ attr_reader :accumulator, :collector, :decoder
17
+
18
+ def initialize(event_stream_owner)
19
+ @owner = event_stream_owner
20
+ reset!
21
+ end
22
+
23
+ def reset!
24
+ @accumulator = RubyLLM::StreamAccumulator.new
25
+ @collector = BlockCollector.new
26
+ @decoder = @owner.event_stream_decoder
27
+ end
28
+
29
+ def reset_if_new_attempt!(chunk, received_bytes)
30
+ reset! if received_bytes && received_bytes == chunk.bytesize
31
+ end
32
+ end
33
+ end
34
+ end