anthropic 1.48.2 → 1.49.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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +1 -1
  4. data/lib/anthropic/client.rb +8 -2
  5. data/lib/anthropic/errors.rb +14 -0
  6. data/lib/anthropic/helpers/aws/client.rb +3 -3
  7. data/lib/anthropic/helpers/aws_auth.rb +39 -29
  8. data/lib/anthropic/helpers/bedrock/client.rb +99 -67
  9. data/lib/anthropic/helpers/bedrock/event_stream.rb +92 -0
  10. data/lib/anthropic/helpers/bedrock/mantle_client.rb +3 -3
  11. data/lib/anthropic/helpers/vertex/client.rb +99 -55
  12. data/lib/anthropic/internal/transport/base_client.rb +196 -33
  13. data/lib/anthropic/internal/util.rb +37 -1
  14. data/lib/anthropic/middleware.rb +435 -0
  15. data/lib/anthropic/models/beta/beta_advisor_tool_20260301.rb +3 -1
  16. data/lib/anthropic/models/beta/beta_code_execution_tool_20250522.rb +3 -1
  17. data/lib/anthropic/models/beta/beta_code_execution_tool_20250825.rb +3 -1
  18. data/lib/anthropic/models/beta/beta_code_execution_tool_20260120.rb +3 -1
  19. data/lib/anthropic/models/beta/beta_code_execution_tool_20260521.rb +86 -0
  20. data/lib/anthropic/models/beta/beta_content_block.rb +3 -3
  21. data/lib/anthropic/models/beta/beta_content_block_param.rb +10 -12
  22. data/lib/anthropic/models/beta/beta_fallback_block.rb +13 -5
  23. data/lib/anthropic/models/beta/beta_fallback_block_param.rb +22 -13
  24. data/lib/anthropic/models/beta/beta_fallback_refusal_trigger.rb +44 -0
  25. data/lib/anthropic/models/beta/beta_memory_tool_20250818.rb +3 -1
  26. data/lib/anthropic/models/beta/beta_raw_content_block_start_event.rb +3 -3
  27. data/lib/anthropic/models/beta/beta_refusal_stop_details.rb +3 -7
  28. data/lib/anthropic/models/beta/beta_tool.rb +3 -1
  29. data/lib/anthropic/models/beta/beta_tool_bash_20241022.rb +3 -1
  30. data/lib/anthropic/models/beta/beta_tool_bash_20250124.rb +3 -1
  31. data/lib/anthropic/models/beta/beta_tool_computer_use_20241022.rb +3 -1
  32. data/lib/anthropic/models/beta/beta_tool_computer_use_20250124.rb +3 -1
  33. data/lib/anthropic/models/beta/beta_tool_computer_use_20251124.rb +3 -1
  34. data/lib/anthropic/models/beta/beta_tool_search_tool_bm25_20251119.rb +3 -1
  35. data/lib/anthropic/models/beta/beta_tool_search_tool_regex_20251119.rb +3 -1
  36. data/lib/anthropic/models/beta/beta_tool_text_editor_20241022.rb +3 -1
  37. data/lib/anthropic/models/beta/beta_tool_text_editor_20250124.rb +3 -1
  38. data/lib/anthropic/models/beta/beta_tool_text_editor_20250429.rb +3 -1
  39. data/lib/anthropic/models/beta/beta_tool_text_editor_20250728.rb +3 -1
  40. data/lib/anthropic/models/beta/beta_tool_union.rb +4 -1
  41. data/lib/anthropic/models/beta/beta_web_fetch_tool_20250910.rb +3 -1
  42. data/lib/anthropic/models/beta/beta_web_fetch_tool_20260209.rb +3 -1
  43. data/lib/anthropic/models/beta/beta_web_fetch_tool_20260309.rb +3 -1
  44. data/lib/anthropic/models/beta/beta_web_search_tool_20250305.rb +3 -1
  45. data/lib/anthropic/models/beta/beta_web_search_tool_20260209.rb +3 -1
  46. data/lib/anthropic/models/beta/beta_webhook_event.rb +2 -2
  47. data/lib/anthropic/models/beta/beta_webhook_event_data.rb +3 -1
  48. data/lib/anthropic/models/beta/beta_webhook_session_updated_event_data.rb +41 -0
  49. data/lib/anthropic/models/beta/message_count_tokens_params.rb +6 -3
  50. data/lib/anthropic/models/beta/message_create_params.rb +2 -2
  51. data/lib/anthropic/models/beta/messages/batch_create_params.rb +2 -2
  52. data/lib/anthropic/models/beta/unwrap_webhook_event.rb +2 -2
  53. data/lib/anthropic/models/code_execution_tool_20250522.rb +3 -1
  54. data/lib/anthropic/models/code_execution_tool_20250825.rb +3 -1
  55. data/lib/anthropic/models/code_execution_tool_20260120.rb +3 -1
  56. data/lib/anthropic/models/code_execution_tool_20260521.rb +82 -0
  57. data/lib/anthropic/models/memory_tool_20250818.rb +3 -1
  58. data/lib/anthropic/models/message_count_tokens_params.rb +2 -2
  59. data/lib/anthropic/models/message_count_tokens_tool.rb +4 -1
  60. data/lib/anthropic/models/message_create_params.rb +2 -2
  61. data/lib/anthropic/models/messages/batch_create_params.rb +2 -2
  62. data/lib/anthropic/models/refusal_stop_details.rb +3 -7
  63. data/lib/anthropic/models/tool.rb +3 -1
  64. data/lib/anthropic/models/tool_bash_20250124.rb +3 -1
  65. data/lib/anthropic/models/tool_search_tool_bm25_20251119.rb +3 -1
  66. data/lib/anthropic/models/tool_search_tool_regex_20251119.rb +3 -1
  67. data/lib/anthropic/models/tool_text_editor_20250124.rb +3 -1
  68. data/lib/anthropic/models/tool_text_editor_20250429.rb +3 -1
  69. data/lib/anthropic/models/tool_text_editor_20250728.rb +3 -1
  70. data/lib/anthropic/models/tool_union.rb +4 -1
  71. data/lib/anthropic/models/web_fetch_tool_20250910.rb +3 -1
  72. data/lib/anthropic/models/web_fetch_tool_20260209.rb +3 -1
  73. data/lib/anthropic/models/web_fetch_tool_20260309.rb +3 -1
  74. data/lib/anthropic/models/web_search_tool_20250305.rb +3 -1
  75. data/lib/anthropic/models/web_search_tool_20260209.rb +3 -1
  76. data/lib/anthropic/models.rb +2 -0
  77. data/lib/anthropic/request_options.rb +9 -0
  78. data/lib/anthropic/resources/beta/messages.rb +3 -3
  79. data/lib/anthropic/resources/messages.rb +3 -3
  80. data/lib/anthropic/version.rb +1 -1
  81. data/lib/anthropic.rb +6 -0
  82. data/rbi/anthropic/client.rbi +7 -2
  83. data/rbi/anthropic/errors.rbi +5 -0
  84. data/rbi/anthropic/helpers/aws/client.rbi +3 -6
  85. data/rbi/anthropic/helpers/bedrock/client.rbi +24 -13
  86. data/rbi/anthropic/helpers/bedrock/event_stream.rbi +25 -0
  87. data/rbi/anthropic/helpers/bedrock/mantle_client.rbi +3 -6
  88. data/rbi/anthropic/helpers/vertex/client.rbi +28 -8
  89. data/rbi/anthropic/internal/transport/base_client.rbi +39 -4
  90. data/rbi/anthropic/internal/util.rbi +5 -0
  91. data/rbi/anthropic/middleware.rbi +338 -0
  92. data/rbi/anthropic/models/beta/beta_advisor_tool_20260301.rbi +7 -1
  93. data/rbi/anthropic/models/beta/beta_code_execution_tool_20250522.rbi +7 -1
  94. data/rbi/anthropic/models/beta/beta_code_execution_tool_20250825.rbi +7 -1
  95. data/rbi/anthropic/models/beta/beta_code_execution_tool_20260120.rbi +7 -1
  96. data/rbi/anthropic/models/beta/beta_code_execution_tool_20260521.rbi +178 -0
  97. data/rbi/anthropic/models/beta/beta_fallback_block.rbi +19 -4
  98. data/rbi/anthropic/models/beta/beta_fallback_block_param.rbi +23 -13
  99. data/rbi/anthropic/models/beta/beta_fallback_refusal_trigger.rbi +108 -0
  100. data/rbi/anthropic/models/beta/beta_memory_tool_20250818.rbi +7 -1
  101. data/rbi/anthropic/models/beta/beta_refusal_stop_details.rbi +3 -9
  102. data/rbi/anthropic/models/beta/beta_tool.rbi +7 -1
  103. data/rbi/anthropic/models/beta/beta_tool_bash_20241022.rbi +7 -1
  104. data/rbi/anthropic/models/beta/beta_tool_bash_20250124.rbi +7 -1
  105. data/rbi/anthropic/models/beta/beta_tool_computer_use_20241022.rbi +7 -1
  106. data/rbi/anthropic/models/beta/beta_tool_computer_use_20250124.rbi +7 -1
  107. data/rbi/anthropic/models/beta/beta_tool_computer_use_20251124.rbi +7 -1
  108. data/rbi/anthropic/models/beta/beta_tool_search_tool_bm25_20251119.rbi +7 -1
  109. data/rbi/anthropic/models/beta/beta_tool_search_tool_regex_20251119.rbi +7 -1
  110. data/rbi/anthropic/models/beta/beta_tool_text_editor_20241022.rbi +7 -1
  111. data/rbi/anthropic/models/beta/beta_tool_text_editor_20250124.rbi +7 -1
  112. data/rbi/anthropic/models/beta/beta_tool_text_editor_20250429.rbi +7 -1
  113. data/rbi/anthropic/models/beta/beta_tool_text_editor_20250728.rbi +7 -1
  114. data/rbi/anthropic/models/beta/beta_tool_union.rbi +1 -0
  115. data/rbi/anthropic/models/beta/beta_web_fetch_tool_20250910.rbi +7 -1
  116. data/rbi/anthropic/models/beta/beta_web_fetch_tool_20260209.rbi +7 -1
  117. data/rbi/anthropic/models/beta/beta_web_fetch_tool_20260309.rbi +7 -1
  118. data/rbi/anthropic/models/beta/beta_web_search_tool_20250305.rbi +7 -1
  119. data/rbi/anthropic/models/beta/beta_web_search_tool_20260209.rbi +7 -1
  120. data/rbi/anthropic/models/beta/beta_webhook_event.rbi +6 -3
  121. data/rbi/anthropic/models/beta/beta_webhook_event_data.rbi +2 -1
  122. data/rbi/anthropic/models/beta/beta_webhook_session_updated_event_data.rbi +63 -0
  123. data/rbi/anthropic/models/beta/message_count_tokens_params.rbi +5 -0
  124. data/rbi/anthropic/models/beta/message_create_params.rbi +4 -0
  125. data/rbi/anthropic/models/beta/messages/batch_create_params.rbi +4 -0
  126. data/rbi/anthropic/models/beta/unwrap_webhook_event.rbi +2 -1
  127. data/rbi/anthropic/models/code_execution_tool_20250522.rbi +7 -1
  128. data/rbi/anthropic/models/code_execution_tool_20250825.rbi +7 -1
  129. data/rbi/anthropic/models/code_execution_tool_20260120.rbi +7 -1
  130. data/rbi/anthropic/models/code_execution_tool_20260521.rbi +168 -0
  131. data/rbi/anthropic/models/memory_tool_20250818.rbi +7 -1
  132. data/rbi/anthropic/models/message_count_tokens_params.rbi +4 -0
  133. data/rbi/anthropic/models/message_count_tokens_tool.rbi +1 -0
  134. data/rbi/anthropic/models/message_create_params.rbi +4 -0
  135. data/rbi/anthropic/models/messages/batch_create_params.rbi +4 -0
  136. data/rbi/anthropic/models/refusal_stop_details.rbi +3 -9
  137. data/rbi/anthropic/models/tool.rbi +7 -1
  138. data/rbi/anthropic/models/tool_bash_20250124.rbi +7 -1
  139. data/rbi/anthropic/models/tool_search_tool_bm25_20251119.rbi +7 -1
  140. data/rbi/anthropic/models/tool_search_tool_regex_20251119.rbi +7 -1
  141. data/rbi/anthropic/models/tool_text_editor_20250124.rbi +7 -1
  142. data/rbi/anthropic/models/tool_text_editor_20250429.rbi +7 -1
  143. data/rbi/anthropic/models/tool_text_editor_20250728.rbi +7 -1
  144. data/rbi/anthropic/models/tool_union.rbi +1 -0
  145. data/rbi/anthropic/models/web_fetch_tool_20250910.rbi +7 -1
  146. data/rbi/anthropic/models/web_fetch_tool_20260209.rbi +7 -1
  147. data/rbi/anthropic/models/web_fetch_tool_20260309.rbi +7 -1
  148. data/rbi/anthropic/models/web_search_tool_20250305.rbi +7 -1
  149. data/rbi/anthropic/models/web_search_tool_20260209.rbi +7 -1
  150. data/rbi/anthropic/models.rbi +2 -0
  151. data/rbi/anthropic/request_options.rbi +5 -0
  152. data/rbi/anthropic/resources/beta/messages.rbi +3 -0
  153. data/rbi/anthropic/resources/messages.rbi +3 -0
  154. data/sig/anthropic/client.rbs +2 -1
  155. data/sig/anthropic/errors.rbs +3 -0
  156. data/sig/anthropic/helpers/bedrock/client.rbs +12 -4
  157. data/sig/anthropic/helpers/vertex/client.rbs +17 -4
  158. data/sig/anthropic/internal/transport/base_client.rbs +18 -3
  159. data/sig/anthropic/internal/util.rbs +2 -0
  160. data/sig/anthropic/middleware.rbs +117 -0
  161. data/sig/anthropic/models/beta/beta_advisor_tool_20260301.rbs +5 -1
  162. data/sig/anthropic/models/beta/beta_code_execution_tool_20250522.rbs +5 -1
  163. data/sig/anthropic/models/beta/beta_code_execution_tool_20250825.rbs +5 -1
  164. data/sig/anthropic/models/beta/beta_code_execution_tool_20260120.rbs +5 -1
  165. data/sig/anthropic/models/beta/beta_code_execution_tool_20260521.rbs +74 -0
  166. data/sig/anthropic/models/beta/beta_fallback_block.rbs +5 -0
  167. data/sig/anthropic/models/beta/beta_fallback_block_param.rbs +9 -2
  168. data/sig/anthropic/models/beta/beta_fallback_refusal_trigger.rbs +42 -0
  169. data/sig/anthropic/models/beta/beta_memory_tool_20250818.rbs +5 -1
  170. data/sig/anthropic/models/beta/beta_tool.rbs +5 -1
  171. data/sig/anthropic/models/beta/beta_tool_bash_20241022.rbs +5 -1
  172. data/sig/anthropic/models/beta/beta_tool_bash_20250124.rbs +5 -1
  173. data/sig/anthropic/models/beta/beta_tool_computer_use_20241022.rbs +5 -1
  174. data/sig/anthropic/models/beta/beta_tool_computer_use_20250124.rbs +5 -1
  175. data/sig/anthropic/models/beta/beta_tool_computer_use_20251124.rbs +5 -1
  176. data/sig/anthropic/models/beta/beta_tool_search_tool_bm25_20251119.rbs +5 -1
  177. data/sig/anthropic/models/beta/beta_tool_search_tool_regex_20251119.rbs +5 -1
  178. data/sig/anthropic/models/beta/beta_tool_text_editor_20241022.rbs +5 -1
  179. data/sig/anthropic/models/beta/beta_tool_text_editor_20250124.rbs +5 -1
  180. data/sig/anthropic/models/beta/beta_tool_text_editor_20250429.rbs +5 -1
  181. data/sig/anthropic/models/beta/beta_tool_text_editor_20250728.rbs +5 -1
  182. data/sig/anthropic/models/beta/beta_tool_union.rbs +1 -0
  183. data/sig/anthropic/models/beta/beta_web_fetch_tool_20250910.rbs +5 -1
  184. data/sig/anthropic/models/beta/beta_web_fetch_tool_20260209.rbs +5 -1
  185. data/sig/anthropic/models/beta/beta_web_fetch_tool_20260309.rbs +5 -1
  186. data/sig/anthropic/models/beta/beta_web_search_tool_20250305.rbs +5 -1
  187. data/sig/anthropic/models/beta/beta_web_search_tool_20260209.rbs +5 -1
  188. data/sig/anthropic/models/beta/beta_webhook_event_data.rbs +1 -0
  189. data/sig/anthropic/models/beta/beta_webhook_session_updated_event_data.rbs +39 -0
  190. data/sig/anthropic/models/beta/message_count_tokens_params.rbs +1 -0
  191. data/sig/anthropic/models/code_execution_tool_20250522.rbs +5 -1
  192. data/sig/anthropic/models/code_execution_tool_20250825.rbs +5 -1
  193. data/sig/anthropic/models/code_execution_tool_20260120.rbs +5 -1
  194. data/sig/anthropic/models/code_execution_tool_20260521.rbs +70 -0
  195. data/sig/anthropic/models/memory_tool_20250818.rbs +5 -1
  196. data/sig/anthropic/models/message_count_tokens_tool.rbs +1 -0
  197. data/sig/anthropic/models/tool.rbs +5 -1
  198. data/sig/anthropic/models/tool_bash_20250124.rbs +5 -1
  199. data/sig/anthropic/models/tool_search_tool_bm25_20251119.rbs +5 -1
  200. data/sig/anthropic/models/tool_search_tool_regex_20251119.rbs +5 -1
  201. data/sig/anthropic/models/tool_text_editor_20250124.rbs +5 -1
  202. data/sig/anthropic/models/tool_text_editor_20250429.rbs +5 -1
  203. data/sig/anthropic/models/tool_text_editor_20250728.rbs +5 -1
  204. data/sig/anthropic/models/tool_union.rbs +1 -0
  205. data/sig/anthropic/models/web_fetch_tool_20250910.rbs +5 -1
  206. data/sig/anthropic/models/web_fetch_tool_20260209.rbs +5 -1
  207. data/sig/anthropic/models/web_fetch_tool_20260309.rbs +5 -1
  208. data/sig/anthropic/models/web_search_tool_20250305.rbs +5 -1
  209. data/sig/anthropic/models/web_search_tool_20260209.rbs +5 -1
  210. data/sig/anthropic/models.rbs +2 -0
  211. data/sig/anthropic/request_options.rbs +4 -1
  212. metadata +19 -2
@@ -37,6 +37,11 @@ module Anthropic
37
37
  #
38
38
  # @param max_retry_delay [Float] The maximum number of seconds to wait before retrying a request
39
39
  #
40
+ # @param middleware [Array<#call>, #call, nil] Per-attempt HTTP around-middleware. See
41
+ # {Anthropic::Middleware}. Middleware sees the canonical Anthropic request shape;
42
+ # the Vertex URL rewrite and OAuth header happen inside the continuation, per
43
+ # attempt.
44
+ #
40
45
  def initialize(
41
46
  region: ENV["CLOUD_ML_REGION"],
42
47
  project_id: ENV["ANTHROPIC_VERTEX_PROJECT_ID"],
@@ -44,7 +49,8 @@ module Anthropic
44
49
  max_retries: DEFAULT_MAX_RETRIES,
45
50
  timeout: DEFAULT_TIMEOUT_IN_SECONDS,
46
51
  initial_retry_delay: DEFAULT_INITIAL_RETRY_DELAY,
47
- max_retry_delay: DEFAULT_MAX_RETRY_DELAY
52
+ max_retry_delay: DEFAULT_MAX_RETRY_DELAY,
53
+ middleware: nil
48
54
  )
49
55
  begin
50
56
  require("googleauth")
@@ -81,6 +87,7 @@ module Anthropic
81
87
  raise ArgumentError.new(message)
82
88
  end
83
89
  @project_id = project_id
90
+ @authorization = nil
84
91
 
85
92
  base_url ||= ENV.fetch(
86
93
  "ANTHROPIC_VERTEX_BASE_URL",
@@ -102,6 +109,7 @@ module Anthropic
102
109
  max_retries: max_retries,
103
110
  initial_retry_delay: initial_retry_delay,
104
111
  max_retry_delay: max_retry_delay,
112
+ middleware: middleware
105
113
  )
106
114
 
107
115
  @messages = Anthropic::Resources::Messages.new(client: self)
@@ -146,58 +154,81 @@ module Anthropic
146
154
  #
147
155
  # @return [Hash{Symbol=>Object}]
148
156
  private def build_request(req, opts)
149
- fit_req_to_vertex_specs!(req)
150
-
151
- request_input = super
152
-
153
- headers = request_input.fetch(:headers)
154
-
155
- unless headers.key?("authorization")
156
- authorization = Google::Auth.get_application_default(["https://www.googleapis.com/auth/cloud-platform"])
157
- request_input.store(:headers, authorization.apply(headers))
157
+ # Id-parameterized routes pass `path` as an Array whose first element
158
+ # is the format string (e.g. `["v1/messages/batches/%1$s", id]`).
159
+ path = Array(req[:path]).first.to_s
160
+ if path.start_with?("v1/messages/batches")
161
+ raise NotImplementedError.new("The Batch API is not supported in the Vertex client yet")
158
162
  end
159
163
 
160
- request_input
164
+ super
161
165
  end
162
166
 
163
- # @private
164
- #
165
- # Overrides request components for Vertex-specific request-shape requirements.
166
- #
167
- # @param request_components [Hash{Symbol=>Object}] .
168
- #
169
- # @option request_components [Symbol] :method
170
- #
171
- # @option request_components [String, Array<String>] :path
172
- #
173
- # @option request_components [Hash{String=>Array<String>, String, nil}, nil] :query
174
- #
175
- # @option request_components [Hash{String=>String, nil}, nil] :headers
176
- #
177
- # @option request_components [Object, nil] :body
167
+ # @api private
178
168
  #
179
- # @option request_components [Symbol, nil] :unwrap
169
+ # The Vertex provider middleware: rewrites the canonical request into
170
+ # Vertex's shape and applies the Google OAuth `authorization` header.
171
+ # Appended innermost on every dispatch (below user middleware) and runs
172
+ # per attempt, so a middleware that re-issues the request gets a fresh
173
+ # token per leg, mirroring the Bedrock SigV4 placement.
180
174
  #
181
- # @option request_components [Class, nil] :page
175
+ # @return [#call]
176
+ private def provider_middleware
177
+ lambda do |req, nxt|
178
+ nxt.call(apply_google_auth(adapt_request(req)))
179
+ end
180
+ end
181
+
182
+ # @api private
182
183
  #
183
- # @option request_components [Anthropic::Converter, Class, nil] :model
184
+ # @param req [Anthropic::APIRequest]
185
+ # @return [Anthropic::APIRequest]
186
+ private def apply_google_auth(req)
187
+ return req if req.headers.key?("authorization")
188
+ # `follow_redirect` stripped `authorization` for a cross-origin hop —
189
+ # don't re-add it and leak the bearer token to the new origin.
190
+ return req if req.metadata[:cross_origin_redirect]
191
+
192
+ # Memoized: the credentials object caches its token and self-refreshes
193
+ # on expiry, so each retry leg still gets a fresh-enough token without
194
+ # re-resolving ADC (a blocking metadata-server/token-endpoint
195
+ # round-trip) on every attempt.
196
+ authorization =
197
+ @authorization ||= Google::Auth.get_application_default(["https://www.googleapis.com/auth/cloud-platform"])
198
+ # `req.headers` may be the deep-frozen hash a middleware saw, and
199
+ # googleauth's `#apply` does `clone` (which preserves frozen) then
200
+ # `[]=` — so it must be handed a fresh, mutable copy.
201
+ req.with(headers: authorization.apply({**req.headers}))
202
+ end
203
+
204
+ # @api private
184
205
  #
185
- # @return [Hash{Symbol=>Object}]
186
- private def fit_req_to_vertex_specs!(request_components)
187
- if (body = request_components[:body]).is_a?(Hash)
206
+ # Rewrites the canonical Anthropic request into Vertex's shape — drops
207
+ # `:model` from the body (keeping `:stream`) and retargets the URL to
208
+ # `projects/{project}/locations/{region}/publishers/anthropic/models/{model}:{rawPredict|streamRawPredict}`.
209
+ # Called from {#provider_middleware}, so user middleware sees the
210
+ # canonical request. Pure: the incoming request, its body, headers,
211
+ # and URI are never mutated (they are reused across retry attempts).
212
+ #
213
+ # @param req [Anthropic::APIRequest]
214
+ # @return [Anthropic::APIRequest]
215
+ private def adapt_request(req)
216
+ body = req.body
217
+ headers = req.headers
218
+ url = req.url
219
+ path = url.path.to_s
220
+ query_ok = url.query.nil? || url.query == "beta=true"
221
+
222
+ if body.is_a?(Hash)
223
+ body = body.dup
188
224
  body[:anthropic_version] ||= DEFAULT_VERSION
189
225
 
190
226
  if (anthropic_beta = body.delete(:"anthropic-beta"))
191
- request_components[:headers] ||= {}
192
- request_components[:headers]["anthropic-beta"] = anthropic_beta.join(",")
227
+ headers = headers.merge("anthropic-beta" => Array(anthropic_beta).join(","))
193
228
  end
194
229
  end
195
230
 
196
- if %w[
197
- v1/messages
198
- v1/messages?beta=true
199
- ].include?(request_components[:path]) && request_components[:method] == :post
200
-
231
+ if req.method == :post && query_ok && path.end_with?("/v1/messages")
201
232
  unless body.is_a?(Hash)
202
233
  raise ArgumentError.new("Expected json data to be a hash for post /v1/messages")
203
234
  end
@@ -205,26 +236,39 @@ module Anthropic
205
236
  model = body.delete(:model)
206
237
  specifier = body[:stream] ? "streamRawPredict" : "rawPredict"
207
238
 
208
- request_components[:path] =
239
+ url = rewrite_path(
240
+ url,
241
+ %r{v1/messages\z},
209
242
  "projects/#{@project_id}/locations/#{region}/publishers/anthropic/models/#{model}:#{specifier}"
210
-
243
+ )
244
+ elsif req.method == :post && query_ok && path.end_with?("/v1/messages/count_tokens")
245
+ url = rewrite_path(
246
+ url,
247
+ %r{v1/messages/count_tokens\z},
248
+ "projects/#{@project_id}/locations/#{region}/publishers/anthropic/" \
249
+ "models/count-tokens:rawPredict"
250
+ )
211
251
  end
212
252
 
213
- if %w[
214
- v1/messages/count_tokens
215
- v1/messages/count_tokens?beta=true
216
- ].include?(request_components[:path]) &&
217
- request_components[:method] == :post
218
- request_components[:path] =
219
- "projects/#{@project_id}/locations/#{region}/publishers/anthropic/models/count-tokens:rawPredict"
220
-
221
- end
222
-
223
- if request_components[:path].start_with?("v1/messages/batches/")
224
- raise AnthropicError("The Batch API is not supported in the Vertex client yet")
225
- end
253
+ return req if body.equal?(req.body) && headers.equal?(req.headers) && url.equal?(req.url)
254
+ req.with(body: body, headers: headers, url: url)
255
+ end
226
256
 
227
- request_components
257
+ # @api private
258
+ #
259
+ # Retarget `url`'s path (replacing `pattern` with `replacement`) and drop
260
+ # its query, returning a fresh copy so the incoming request's URI is left
261
+ # untouched.
262
+ #
263
+ # @param url [URI::Generic]
264
+ # @param pattern [Regexp]
265
+ # @param replacement [String]
266
+ # @return [URI::Generic]
267
+ private def rewrite_path(url, pattern, replacement)
268
+ url = url.dup
269
+ url.path = url.path.to_s.sub(pattern, replacement)
270
+ url.query = nil
271
+ url
228
272
  end
229
273
  end
230
274
  end
@@ -132,7 +132,14 @@ module Anthropic
132
132
  # from undici
133
133
  if Anthropic::Internal::Util.uri_origin(url) != Anthropic::Internal::Util.uri_origin(location)
134
134
  drop = %w[authorization cookie host proxy-authorization]
135
- request = {**request, headers: request.fetch(:headers).except(*drop)}
135
+ # The sentinel keeps the strip durable: provider middleware
136
+ # (Vertex OAuth, Bedrock SigV4) runs per attempt and would
137
+ # otherwise re-add `authorization` on the redirected leg.
138
+ request = {
139
+ **request,
140
+ headers: request.fetch(:headers).except(*drop),
141
+ metadata: {**request.fetch(:metadata, {}), cross_origin_redirect: true}
142
+ }
136
143
  end
137
144
 
138
145
  request
@@ -178,6 +185,9 @@ module Anthropic
178
185
  # @return [Anthropic::Internal::Transport::PooledNetRequester]
179
186
  attr_reader :requester
180
187
 
188
+ # @return [Array<#call>] the middleware chain (first = outermost).
189
+ attr_reader :middleware
190
+
181
191
  # @api private
182
192
  #
183
193
  # @param base_url [String]
@@ -187,6 +197,8 @@ module Anthropic
187
197
  # @param max_retry_delay [Float]
188
198
  # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
189
199
  # @param idempotency_header [String, nil]
200
+ # @param middleware [Array<#call>, #call, nil]
201
+ # @param requester [Anthropic::Internal::Transport::PooledNetRequester, nil]
190
202
  def initialize(
191
203
  base_url:,
192
204
  timeout: 0.0,
@@ -194,9 +206,12 @@ module Anthropic
194
206
  initial_retry_delay: 0.0,
195
207
  max_retry_delay: 0.0,
196
208
  headers: {},
197
- idempotency_header: nil
209
+ idempotency_header: nil,
210
+ middleware: nil,
211
+ requester: nil
198
212
  )
199
- @requester = Anthropic::Internal::Transport::PooledNetRequester.new
213
+ @middleware = Array(middleware).freeze
214
+ @requester = requester || Anthropic::Internal::Transport::PooledNetRequester.new
200
215
  @headers = Anthropic::Internal::Util.normalized_headers(
201
216
  self.class::PLATFORM_HEADERS,
202
217
  {
@@ -316,15 +331,30 @@ module Anthropic
316
331
  @base_url_components,
317
332
  {**req, path: path, query: query}
318
333
  )
319
- headers, encoded = Anthropic::Internal::Util.encode_content(headers, body)
334
+
335
+ # Per-request middleware rides in `request_options` but is kept out of
336
+ # the `options` view a middleware sees — it is the live chain, not
337
+ # serializable request configuration, and surfacing it there would be
338
+ # self-referential.
339
+ request_middleware = Array(opts[:middleware])
340
+
320
341
  {
321
342
  method: method,
322
343
  url: url,
323
344
  headers: headers,
324
- body: encoded,
345
+ body: body,
325
346
  max_retries: opts.fetch(:max_retries, @max_retries),
326
347
  timeout: timeout,
327
- user_header_keys: user_header_keys
348
+ user_header_keys: user_header_keys,
349
+ # For paginated requests `model` is the page's *item* type, not the
350
+ # response envelope's — middleware `parse` falls back to the raw
351
+ # decoded page data instead of mis-coercing.
352
+ cast_to: req[:page] ? nil : req.fetch(:model, Anthropic::Internal::Type::Unknown),
353
+ stream: req[:stream],
354
+ unwrap: req[:unwrap],
355
+ options: opts.except(:middleware),
356
+ middleware: request_middleware,
357
+ metadata: {}
328
358
  }
329
359
  end
330
360
 
@@ -356,20 +386,24 @@ module Anthropic
356
386
 
357
387
  # @api private
358
388
  #
359
- # Very private API, do not use
389
+ # The default innermost middleware. 3p provider clients
390
+ # (Bedrock/Vertex/AWS) override this to return a `#call(req, nxt)`
391
+ # entry that rewrites the canonical Anthropic request into the
392
+ # provider's wire shape and applies provider auth (SigV4 signing,
393
+ # OAuth tokens).
360
394
  #
361
- # @param request [Hash{Symbol=>Object}] .
395
+ # It is appended below all user middleware on every dispatch — client-
396
+ # and request-level entries alike always see the canonical request
397
+ # (`body[:model]`, `/v1/messages` URL) regardless of provider
398
+ # ("3p-inner" ordering) — and, like any middleware, runs per attempt, so
399
+ # SDK retries and middleware-re-issued requests are re-adapted and
400
+ # re-signed for their own leg.
362
401
  #
363
- # @option request [Symbol] :method
402
+ # The requests it receives are reused across retry attempts — it MUST
403
+ # be pure and must not mutate `req` or any object reachable from it.
364
404
  #
365
- # @option request [URI::Generic] :url
366
- #
367
- # @option request [Hash{String=>String}] :headers
368
- #
369
- # @option request [Object] :body
370
- #
371
- # @return [Hash{Symbol, Object}]
372
- private def transform_request(request) = request
405
+ # @return [#call, nil]
406
+ private def provider_middleware = nil
373
407
 
374
408
  # @api private
375
409
  #
@@ -381,6 +415,28 @@ module Anthropic
381
415
  self.class.should_retry?(status, headers: headers)
382
416
  end
383
417
 
418
+ # @api private
419
+ #
420
+ # Whether `err` (or any error reachable via `Exception#cause`) is one
421
+ # the SDK retries — {Anthropic::Errors::RetryableError},
422
+ # {Anthropic::Errors::APIConnectionError}, or its subclass
423
+ # {Anthropic::Errors::APITimeoutError}.
424
+ #
425
+ # @param err [Exception]
426
+ # @return [Boolean]
427
+ private def retryable_error?(err)
428
+ seen = {}.compare_by_identity
429
+ while err && !seen[err]
430
+ seen[err] = true
431
+ case err
432
+ when Anthropic::Errors::RetryableError, Anthropic::Errors::APIConnectionError
433
+ return true
434
+ end
435
+ err = err.cause
436
+ end
437
+ false
438
+ end
439
+
384
440
  # @api private
385
441
  #
386
442
  # @param request [Hash{Symbol=>Object}] .
@@ -404,42 +460,137 @@ module Anthropic
404
460
  # @param send_retry_header [Boolean]
405
461
  #
406
462
  # @raise [Anthropic::Errors::APIError]
407
- # @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
463
+ # @return [Array(Integer, Net::HTTPResponse|nil, Hash{String=>String}, Enumerable<String>)]
408
464
  def send_request(request, redirect_count:, retry_count:, send_retry_header:)
409
465
  if send_retry_header
410
466
  request.fetch(:headers)["x-stainless-retry-count"] = retry_count.to_s
411
467
  end
412
468
 
413
- request = transform_request(request)
414
469
  url, max_retries, timeout = request.fetch_values(:url, :max_retries, :timeout)
415
- input = {**request.except(:timeout), deadline: Anthropic::Internal::Util.monotonic_secs + timeout}
470
+
471
+ # Request-level middleware (from `request_options`) runs innermost
472
+ # among user middleware — below client-level entries, above the
473
+ # provider middleware.
474
+ request_middleware = request.fetch(:middleware, [])
475
+
476
+ # Per-attempt defensive copies: the `request` hash and its members are
477
+ # reused across retry recursions (the SDK mutates `request[:headers]`
478
+ # for retry-count/auth stamping), so the immutable view middleware sees
479
+ # must not alias them. With no user middleware at all only the terminal
480
+ # and the provider middleware read these (both are pure), so the deep
481
+ # freeze is skipped. Only `headers` change across retries, so the frozen
482
+ # `url`/`body`/`options` copies are cached on `request` and reused; the
483
+ # redirect path drops the stale `url`/`body` copies. `metadata` is the
484
+ # deliberate exception — it is the cross-attempt scratchpad a middleware
485
+ # may write to.
486
+ user_middleware_empty = @middleware.empty? && request_middleware.empty?
487
+ freeze = user_middleware_empty ? ->(o) { o } : Anthropic::Internal::Util.method(:deep_frozen_copy)
488
+ api_req = Anthropic::APIRequest.new(
489
+ method: request.fetch(:method),
490
+ url: (request[:frozen_url] ||= freeze.call(url)),
491
+ headers: freeze.call(request.fetch(:headers)),
492
+ body: (request[:frozen_body] ||= freeze.call(request[:body])),
493
+ stream: request[:stream],
494
+ cast_to: request[:cast_to],
495
+ unwrap: request[:unwrap],
496
+ options: (
497
+ request[:frozen_options] ||= freeze.call(request.fetch(:options, {}).merge(timeout: timeout))
498
+ ),
499
+ retry_count: retry_count,
500
+ metadata: request.fetch(:metadata, {})
501
+ )
502
+
503
+ # The provider request the response actually came from — captured
504
+ # inside the terminal so error reporting, relative-redirect
505
+ # resolution, and redirect re-sends see the post-`provider_middleware`
506
+ # (Bedrock/Vertex) URL and body, not the canonical `/v1/messages`
507
+ # shape. Falls back to the canonical values when middleware
508
+ # short-circuits before `nxt.call`.
509
+ adapted_url = url
510
+ adapted_body = request[:body]
511
+ attempt_body = nil
512
+ attempt_headers = nil
513
+ terminal = lambda do |r|
514
+ adapted_url = r.url
515
+ adapted_body = r.body
516
+ enc_headers, encoded = Anthropic::Internal::Util.encode_content(r.headers, r.body)
517
+ input = {
518
+ method: r.method,
519
+ url: r.url,
520
+ headers: enc_headers,
521
+ body: encoded,
522
+ deadline: Anthropic::Internal::Util.monotonic_secs + timeout
523
+ }
524
+ http_status, raw, body = @requester.execute(input)
525
+ attempt_body = body
526
+ res = Anthropic::APIResponse.wrap(http_status, raw, body, request: r)
527
+ attempt_headers = res.headers
528
+ res
529
+ end
416
530
 
417
531
  begin
418
- status, response, stream = @requester.execute(input)
419
- rescue Anthropic::Errors::APIConnectionError => e
532
+ chain = Anthropic::Middleware.build_chain(
533
+ [*@middleware, *request_middleware, *provider_middleware],
534
+ terminal
535
+ )
536
+ mres = chain.call(api_req)
537
+ unless mres.is_a?(Anthropic::APIResponse)
538
+ raise TypeError,
539
+ "middleware returned #{mres.class}, expected Anthropic::APIResponse"
540
+ end
541
+ status, response, headers, stream = mres.to_tuple
542
+ rescue StandardError => e
543
+ # A middleware may raise after `nxt.call` returned a live response
544
+ # (e.g. `raise RetryableError if res.status >= 500`); release the
545
+ # pooled connection its un-consumed body still holds.
546
+ Anthropic::Internal::Util.close_fused!(attempt_body)
547
+ raise unless retryable_error?(e)
420
548
  status = e
549
+ # A middleware may raise after observing a live response (e.g. a 429
550
+ # carrying `Retry-After`); keep that response's headers so retry
551
+ # backoff still honors server-driven delay. `nil` when the terminal
552
+ # itself raised — a connection failure carries no response.
553
+ headers = attempt_headers || {}
421
554
  end
422
- headers = Anthropic::Internal::Util.normalized_headers(response&.each_header&.to_h)
423
555
 
424
556
  case status
425
557
  in ..299
426
- [status, response, stream]
558
+ [status, response, headers, stream]
427
559
  in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
428
560
  self.class.reap_connection!(status, stream: stream)
429
561
 
430
562
  message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
431
- raise Anthropic::Errors::APIConnectionError.new(url: url, response: response, message: message)
563
+ raise Anthropic::Errors::APIConnectionError.new(
564
+ url: adapted_url,
565
+ response: response,
566
+ message: message
567
+ )
432
568
  in 300..399
433
569
  self.class.reap_connection!(status, stream: stream)
434
570
 
435
- request = self.class.follow_redirect(request, status: status, response_headers: headers)
571
+ # Re-send the provider-adapted body: the redirect target is a
572
+ # provider URL that fails the provider middleware's adapt gate, so
573
+ # the canonical body would otherwise go out un-adapted on
574
+ # Bedrock/Vertex. A SigV4-signed `StringIO` body was consumed by
575
+ # the prior attempt and must be rewound before re-signing.
576
+ adapted_body.rewind if adapted_body.is_a?(StringIO) || adapted_body.is_a?(IO)
577
+ request = self.class.follow_redirect(
578
+ request.merge(url: adapted_url, body: adapted_body),
579
+ status: status,
580
+ response_headers: headers
581
+ )
582
+ # The redirect retargets the URL, and `follow_redirect` may drop the
583
+ # body (303 → GET); the cached frozen copies would then be stale, so
584
+ # they are recomputed on the redirected attempt.
585
+ request.delete(:frozen_body)
586
+ request.delete(:frozen_url)
436
587
  send_request(
437
588
  request,
438
589
  redirect_count: redirect_count + 1,
439
590
  retry_count: retry_count,
440
591
  send_retry_header: send_retry_header
441
592
  )
442
- in Anthropic::Errors::APIConnectionError if retry_count >= max_retries
593
+ in Exception if retry_count >= max_retries
443
594
  raise status
444
595
  in (400..) if retry_count >= max_retries || !retry_request?(status, headers: headers)
445
596
  decoded = Kernel.then do
@@ -449,17 +600,17 @@ module Anthropic
449
600
  end
450
601
 
451
602
  raise Anthropic::Errors::APIStatusError.for(
452
- url: url,
603
+ url: adapted_url,
453
604
  status: status,
454
605
  headers: headers,
455
606
  body: decoded,
456
607
  request: nil,
457
608
  response: response
458
609
  )
459
- in (400..) | Anthropic::Errors::APIConnectionError
610
+ in (400..) | Exception
460
611
  self.class.reap_connection!(status, stream: stream)
461
612
 
462
- delay = retry_delay(response || {}, retry_count: retry_count)
613
+ delay = retry_delay(headers, retry_count: retry_count)
463
614
  sleep(delay)
464
615
 
465
616
  # Refresh auth headers across retries: credential providers (e.g. OAuth
@@ -533,14 +684,13 @@ module Anthropic
533
684
 
534
685
  # Don't send the current retry count in the headers if the caller modified the header defaults.
535
686
  send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
536
- status, response, stream = send_request(
687
+ status, response, headers, stream = send_request(
537
688
  request,
538
689
  redirect_count: 0,
539
690
  retry_count: 0,
540
691
  send_retry_header: send_retry_header
541
692
  )
542
693
 
543
- headers = Anthropic::Internal::Util.normalized_headers(response.each_header.to_h)
544
694
  decoded = Anthropic::Internal::Util.decode_content(headers, stream: stream)
545
695
  case req
546
696
  in {stream: Class => st}
@@ -613,7 +763,20 @@ module Anthropic
613
763
  body: T.anything,
614
764
  max_retries: Integer,
615
765
  timeout: Float,
616
- user_header_keys: T::Array[String]
766
+ user_header_keys: T::Array[String],
767
+ cast_to: T.nilable(Anthropic::Internal::Type::Converter::Input),
768
+ stream: T.nilable(T::Class[T.anything]),
769
+ unwrap: T.nilable(
770
+ T.any(
771
+ Symbol,
772
+ Integer,
773
+ T::Array[T.any(Symbol, Integer)],
774
+ T.proc.params(arg0: T.anything).returns(T.anything)
775
+ )
776
+ ),
777
+ options: T::Hash[Symbol, T.anything],
778
+ middleware: T::Array[T.anything],
779
+ metadata: T::Hash[Symbol, T.anything]
617
780
  }
618
781
  end
619
782
  end
@@ -27,6 +27,38 @@ module Anthropic
27
27
  end
28
28
 
29
29
  class << self
30
+ # @api private
31
+ #
32
+ # Structurally copy and freeze `obj` (Hash/Array recursively; mutable
33
+ # `String` leaves dup-frozen; `URI::Generic` re-parsed with each
34
+ # component string frozen) so the result is fully de-aliased from the
35
+ # input and any in-place mutation raises `FrozenError`. Other leaves
36
+ # (Integer/Symbol — already frozen; typed models; IO) pass through, IO
37
+ # because it must stay live for streamed uploads.
38
+ #
39
+ # @param obj [Object]
40
+ # @return [Object]
41
+ def deep_frozen_copy(obj)
42
+ case obj
43
+ when Hash
44
+ obj.transform_values { deep_frozen_copy(_1) }.freeze
45
+ when Array
46
+ obj.map { deep_frozen_copy(_1) }.freeze
47
+ when String
48
+ obj.frozen? ? obj : obj.dup.freeze
49
+ when URI::Generic
50
+ # Re-parse de-aliases every component string; freezing each via its
51
+ # public reader (then the URI) makes `url.path << "x"` raise without
52
+ # reaching below `URI::Generic`'s public surface. Callers `dup` then
53
+ # reassign components, so a frozen URI is fine to derive from.
54
+ URI.parse(obj.to_s).tap do |u|
55
+ [u.scheme, u.userinfo, u.host, u.path, u.query, u.opaque, u.fragment].each { _1&.freeze }
56
+ end.freeze
57
+ else
58
+ obj
59
+ end
60
+ end
61
+
30
62
  # @api private
31
63
  #
32
64
  # @return [String]
@@ -658,7 +690,11 @@ module Anthropic
658
690
  [headers, JSON.generate(body)]
659
691
  in [Anthropic::Internal::Util::JSONL_CONTENT, Enumerable] unless Anthropic::Internal::Type::FileInput === body
660
692
  [headers, body.lazy.map { JSON.generate(_1) }]
661
- in [%r{^multipart/form-data}, Hash | Anthropic::Internal::Type::FileInput]
693
+ # A `boundary=` already in the content-type means the body was
694
+ # encoded upstream (e.g. SigV4 signing encodes-then-signs and the
695
+ # terminal calls this again) — fall through to the pass-through arms
696
+ # instead of re-wrapping the signed bytes with a second boundary.
697
+ in [%r{^multipart/form-data}, Hash | Anthropic::Internal::Type::FileInput] unless content_type.include?("boundary=")
662
698
  boundary, strio = encode_multipart_streaming(body)
663
699
  headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"}
664
700
  [headers, strio]