diff --git a/lib/anthropic/internal/transport/base_client.rb b/lib/anthropic/internal/transport/base_client.rb index fbe111482..8382583ff 100644 --- a/lib/anthropic/internal/transport/base_client.rb +++ b/lib/anthropic/internal/transport/base_client.rb @@ -528,7 +528,12 @@ def request(req) page.new(client: self, req: req, headers: headers, page_data: decoded) else unwrapped = Anthropic::Internal::Util.dig(decoded, unwrap) - Anthropic::Internal::Type::Converter.coerce(model, unwrapped) + coerced = Anthropic::Internal::Type::Converter.coerce(model, unwrapped) + if coerced.respond_to?(:_status=) + coerced._status = status + coerced._headers = headers + end + coerced end end diff --git a/lib/anthropic/internal/type/base_model.rb b/lib/anthropic/internal/type/base_model.rb index 131b43c14..a8d0c5be3 100644 --- a/lib/anthropic/internal/type/base_model.rb +++ b/lib/anthropic/internal/type/base_model.rb @@ -469,12 +469,30 @@ def to_json(*a) = Anthropic::Internal::Type::Converter.dump(self.class, self).to # @return [String] def to_yaml(*a) = Anthropic::Internal::Type::Converter.dump(self.class, self).to_yaml(*a) + # @api public + # + # Returns the HTTP response status code, if this object was returned from an API + # response. + # + # @return [Integer, nil] + attr_accessor :_status + + # @api public + # + # Returns the HTTP response headers, if this object was returned from an API + # response. + # + # @return [Hash{String=>String}, nil] + attr_accessor :_headers + # Create a new instance of a model. # # @param data [Hash{Symbol=>Object}, self] def initialize(data = {}) @data = {} @coerced = {} + @_status = nil + @_headers = nil Anthropic::Internal::Util.coerce_hash!(data).each do if self.class.known_fields.key?(_1) public_send(:"#{_1}=", _2) diff --git a/test/anthropic/resources/messages/response_headers_test.rb b/test/anthropic/resources/messages/response_headers_test.rb new file mode 100644 index 000000000..64bcdedb2 --- /dev/null +++ b/test/anthropic/resources/messages/response_headers_test.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class Anthropic::Test::Resources::Messages::ResponseHeadersTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + def before_all + super + WebMock.enable! + end + + def after_all + WebMock.disable! + super + end + + def setup + super + @client = Anthropic::Client.new(base_url: "http://localhost", api_key: "test-key") + end + + def teardown + WebMock.reset! + super + end + + def test_non_streaming_response_headers + stub_request(:post, "http://localhost/v1/messages") + .with( + headers: { + "Content-Type" => "application/json" + } + ) + .to_return( + status: 200, + headers: { + "Content-Type" => "application/json", + "anthropic-ratelimit-requests-limit" => "5", + "anthropic-ratelimit-requests-remaining" => "4", + "anthropic-ratelimit-tokens-limit" => "24000", + "anthropic-ratelimit-tokens-remaining" => "24000", + "anthropic-ratelimit-input-tokens-remaining" => "20000", + "anthropic-ratelimit-output-tokens-remaining" => "4000", + "request-id" => "req_abc123" + }, + body: { + id: "msg_123", + type: "message", + role: "assistant", + content: [ + {type: "text", text: "Hello!"} + ], + model: "claude-opus-4-6", + stop_reason: "end_turn", + stop_sequence: nil, + usage: {input_tokens: 10, output_tokens: 5, cache_creation_input_tokens: 0, cache_read_input_tokens: 0} + }.to_json + ) + + response = @client.messages.create( + max_tokens: 1024, + messages: [{role: "user", content: "Hello"}], + model: "claude-opus-4-6" + ) + + assert_instance_of(Anthropic::Message, response) + assert_equal("msg_123", response.id) + + # Verify HTTP status is accessible + assert_equal(200, response._status) + + # Verify HTTP headers are accessible + refute_nil(response._headers) + assert_equal("5", response._headers["anthropic-ratelimit-requests-limit"]) + assert_equal("4", response._headers["anthropic-ratelimit-requests-remaining"]) + assert_equal("24000", response._headers["anthropic-ratelimit-tokens-limit"]) + assert_equal("24000", response._headers["anthropic-ratelimit-tokens-remaining"]) + assert_equal("20000", response._headers["anthropic-ratelimit-input-tokens-remaining"]) + assert_equal("4000", response._headers["anthropic-ratelimit-output-tokens-remaining"]) + assert_equal("req_abc123", response._headers["request-id"]) + end + + def test_non_streaming_response_status + stub_request(:post, "http://localhost/v1/messages") + .to_return( + status: 200, + headers: {"Content-Type" => "application/json"}, + body: { + id: "msg_456", + type: "message", + role: "assistant", + content: [{type: "text", text: "Hi!"}], + model: "claude-opus-4-6", + stop_reason: "end_turn", + stop_sequence: nil, + usage: {input_tokens: 5, output_tokens: 3, cache_creation_input_tokens: 0, cache_read_input_tokens: 0} + }.to_json + ) + + response = @client.messages.create( + max_tokens: 1024, + messages: [{role: "user", content: "Hi"}], + model: "claude-opus-4-6" + ) + + assert_equal(200, response._status) + end +end