Class: Anthropic::APIResponse

Inherits:
Object
  • Object
show all
Defined in:
lib/anthropic/middleware.rb

Overview

The per-attempt response object a middleware sees.

‘nxt.call(req)` returns an `APIResponse` for all HTTP status codes —4xx and 5xx included. It does not raise on error status; the SDK converts to a typed `APIError` only after the chain completes. This lets a middleware inspect `res.status` on a 5xx and choose to substitute, retry differently, or pass through. Connection-level failures (Errors::APITimeoutError, Errors::APIConnectionError) do raise from `nxt.call`.

Defined Under Namespace

Classes: ConsumedBodyError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(status:, headers: {}, body: nil, raw: nil, streaming: nil, request: nil) ⇒ APIResponse

Returns a new instance of APIResponse.

Parameters:

  • status (Integer)
  • headers (Hash{String=>String}) (defaults to: {})
  • body (Enumerable<String>, String, nil) (defaults to: nil)
  • raw (Net::HTTPResponse, nil) (defaults to: nil)
  • streaming (Boolean, nil) (defaults to: nil)

    override ‘streaming?`; inferred from `request` if `nil`

  • request (Anthropic::APIRequest, nil) (defaults to: nil)


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/anthropic/middleware.rb', line 195

def initialize(status:, headers: {}, body: nil, raw: nil, streaming: nil, request: nil)
  @status = status
  @headers = Anthropic::Internal::Util.normalized_headers(headers)
  @raw = raw
  @request = request
  @streaming = streaming
  @buffered = nil
  @drained = false
  @parsed = Anthropic::Internal::OMIT

  @body =
    case body
    in nil
      @buffered = [].freeze
    in String
      @buffered = [body].freeze
    in Array
      @buffered = body.dup.freeze
    else
      # Wrap once so the first pull flips `@drained`; `to_tuple`/`buffer!`
      # then detect a body that was iterated without buffering. Fused so
      # `Util.close_fused!` (stream `close`, connection reaping) propagates
      # to the upstream enum without first pulling a chunk off the socket.
      Anthropic::Internal::Util.chain_fused(body) do |y|
        @drained = true
        body.each { y << _1 }
      end
    end
end

Instance Attribute Details

#headersHash{String=>String} (readonly)

Returns normalized response headers.

Returns:

  • (Hash{String=>String})

    normalized response headers



161
162
163
# File 'lib/anthropic/middleware.rb', line 161

def headers
  @headers
end

#rawNet::HTTPResponse? (readonly)

Returns the raw response. ‘nil` for an `APIResponse.new(…)` constructed by a middleware (e.g. a mock).

Returns:

  • (Net::HTTPResponse, nil)

    the raw response. ‘nil` for an `APIResponse.new(…)` constructed by a middleware (e.g. a mock).



165
166
167
# File 'lib/anthropic/middleware.rb', line 165

def raw
  @raw
end

#requestAnthropic::APIRequest? (readonly)

Returns the request that produced this response. #parse uses it to recover the SDK return type; a fabricated response without one parses to the decoded body only.

Returns:

  • (Anthropic::APIRequest, nil)

    the request that produced this response. #parse uses it to recover the SDK return type; a fabricated response without one parses to the decoded body only.



170
171
172
# File 'lib/anthropic/middleware.rb', line 170

def request
  @request
end

#statusInteger (readonly)

Returns HTTP status code.

Returns:

  • (Integer)

    HTTP status code



158
159
160
# File 'lib/anthropic/middleware.rb', line 158

def status
  @status
end

Class Method Details

.wrap(status, raw, stream, request: nil) ⇒ Anthropic::APIResponse

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wraps the ‘[status, Net::HTTPResponse, Enumerable<String>]` tuple the pooled requester returns.

Parameters:

  • status (Integer)
  • raw (Net::HTTPResponse)
  • stream (Enumerable<String>)
  • request (Anthropic::APIRequest, nil) (defaults to: nil)

Returns:



182
183
184
185
186
# File 'lib/anthropic/middleware.rb', line 182

def self.wrap(status, raw, stream, request: nil)
  # `initialize` normalizes `headers`, so pass the raw header hash straight
  # through rather than normalizing twice on every response.
  new(status: status, headers: raw.each_header.to_h, body: stream, raw: raw, request: request)
end

Instance Method Details

#bodyEnumerable<String>

Returns the response body. Single-consumer unless #buffer! has been called. After ‘buffer!`, this is a rewindable `Array`.

Returns:

  • (Enumerable<String>)

    the response body. Single-consumer unless #buffer! has been called. After ‘buffer!`, this is a rewindable `Array`.



228
# File 'lib/anthropic/middleware.rb', line 228

def body = @buffered || @body

#buffer!(force: false) ⇒ self

Drain the body into memory so it can be re-read. Idempotent once buffered.

Parameters:

  • force (Boolean) (defaults to: false)

    allow buffering a streaming response. By default this raises because buffering an SSE stream defeats streaming; pass ‘force: true` if you know the stream is bounded.

Returns:

  • (self)

Raises:



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/anthropic/middleware.rb', line 254

def buffer!(force: false)
  return self if @buffered

  if streaming? && !force
    raise ArgumentError,
          "Refusing to buffer a streaming response; pass `force: true` " \
          "or use `wrap_body` to transform the stream without buffering."
  end

  if @drained
    raise ConsumedBodyError,
          "Response body was already consumed. Call `buffer!` (or " \
          "`parse`) before reading the body, or use `wrap_body` to " \
          "transform the stream without consuming it."
  end

  @buffered = @body.to_a.freeze
  @drained = true
  self
end

#parseObject

Decode and coerce the body to the SDK-typed result the original caller would have received (e.g. ‘Anthropic::Message`). Internally `buffer!`s first, so calling `parse` does not steal the body from downstream — the SDK reuses the buffered copy.

For non-streaming responses the decoded value is coerced to the request’s return type and memoized — repeated calls across the chain cost a single decode. A fabricated response without a ‘request:` parses to the decoded body (e.g. a `Hash`) instead of an SDK model.

For streaming requests, returns a typed stream reading an independent buffered copy of the response body — iterating (or ‘break`ing out of) it neither consumes nor cancels the events the SDK caller will read. Streams are single-consumer, so each call returns a fresh stream rather than a memoized one.

Examples:

usage_logger = lambda do |req, nxt|
  res = nxt.call(req)
  LOGGER.info("usage: #{res.parse.usage}") if res.status < 300
  res
end

Returns:

  • (Object)


319
320
321
322
323
324
325
326
327
328
# File 'lib/anthropic/middleware.rb', line 319

def parse
  return parse_stream if streaming?
  return @parsed unless Anthropic::Internal::OMIT.equal?(@parsed)

  buffer!
  decoded = Anthropic::Internal::Util.decode_content(@headers, stream: body.each)
  unwrapped = Anthropic::Internal::Util.dig(decoded, @request&.unwrap)
  cast_to = @request&.cast_to || Anthropic::Internal::Type::Unknown
  @parsed = Anthropic::Internal::Type::Converter.coerce(cast_to, unwrapped)
end

#retryable?Boolean

Returns whether the SDK would retry this status under its default policy — parity with the Python SDK’s ‘Response.is_retryable`. Useful for middleware that wants to match the SDK’s own retry decision.

Returns:

  • (Boolean)

    whether the SDK would retry this status under its default policy — parity with the Python SDK’s ‘Response.is_retryable`. Useful for middleware that wants to match the SDK’s own retry decision.



240
241
242
# File 'lib/anthropic/middleware.rb', line 240

def retryable?
  Anthropic::Internal::Transport::BaseClient.should_retry?(@status, headers: @headers)
end

#streaming?Boolean

Returns whether the request that produced this response is a streaming/SSE request.

Returns:

  • (Boolean)

    whether the request that produced this response is a streaming/SSE request.



232
233
234
235
# File 'lib/anthropic/middleware.rb', line 232

def streaming?
  return @streaming unless @streaming.nil?
  @request&.streaming? || false
end

#to_tupleArray(Integer, Net::HTTPResponse|nil, Hash{String=>String}, Enumerable<String>)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Back to the ‘[Integer, Net::HTTPResponse|nil, Hash, Enumerable<String>]` tuple shape `send_request` works with internally. Headers come from #headers — never re-derived from `raw` — so a middleware that returns a modified header set over the original raw response is honored.

Returns:

  • (Array(Integer, Net::HTTPResponse|nil, Hash{String=>String}, Enumerable<String>))


338
339
340
341
342
343
344
345
# File 'lib/anthropic/middleware.rb', line 338

def to_tuple
  if @buffered.nil? && @drained
    raise ConsumedBodyError,
          "Middleware consumed the response body without buffering it. " \
          "Call `buffer!` (or `parse`) before reading, or use `wrap_body`."
  end
  [status, @raw, @headers, @buffered || @body]
end

#wrap_body(streaming: streaming?) ) {|upstream| ... } ⇒ Anthropic::APIResponse

Replace the body with the block’s return value — the composable way to intercept streamed chunks without buffering.

Parameters:

  • streaming (Boolean) (defaults to: streaming?) )

    value for the new response’s ‘streaming?`. Pass `false` if the wrapper collapses the stream to a buffered body.

Yield Parameters:

  • upstream (Enumerable<String>)

Yield Returns:

  • (Enumerable<String>)

Returns:

Raises:

  • (ArgumentError)


283
284
285
286
287
288
289
290
291
292
293
# File 'lib/anthropic/middleware.rb', line 283

def wrap_body(streaming: streaming?)
  raise ArgumentError, "wrap_body requires a block" unless block_given?
  self.class.new(
    status: @status,
    headers: @headers,
    body: yield(body),
    raw: @raw,
    streaming: streaming,
    request: @request
  )
end