anthropic 1.53.0 → 1.54.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/lib/anthropic/helpers/refusal_fallback.rb +633 -0
- data/lib/anthropic/helpers/stainless_helper_header.rb +1 -0
- data/lib/anthropic/request_options.rb +9 -0
- data/lib/anthropic/version.rb +1 -1
- data/lib/anthropic.rb +1 -0
- data/rbi/anthropic/helpers/refusal_fallback.rbi +46 -0
- data/rbi/anthropic/request_options.rbi +6 -0
- data/rbi/lib/anthropic/helpers/stainless_helper_header.rbi +2 -0
- data/sig/anthropic/helpers/refusal_fallback.rbs +24 -0
- data/sig/anthropic/request_options.rbs +4 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9ca7e380442d7abed6048df2f97b8e433050c70b5059ab39336a1ae23756721
|
|
4
|
+
data.tar.gz: a024415de2debc831abfc58a0c4c6568af691a2273d25bf3ac3571a4a3f1eb64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5b653770331cefb105622a73100a27eee59b88d248956ca6633f031bb28b681724259f50302b4a404560ba0d3862686db17c4b148f704dcb49e7d076a00390a
|
|
7
|
+
data.tar.gz: 63b9c7b7edbba3c7eacc95a860d287e188290782be1c63d809fe7e85ecb566121ec4bbeceb8d4c12478195cf5e7467a2e9bd1679ddb62f57223945eff8437c68
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.54.0 (2026-07-01)
|
|
4
|
+
|
|
5
|
+
Full Changelog: [v1.53.0...v1.54.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.53.0...v1.54.0)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **client:** add client-side fallbacks middleware for API providers that do not support server-side fallbacks ([#35](https://github.com/anthropics/anthropic-sdk-ruby/issues/35)) ([880a6b9](https://github.com/anthropics/anthropic-sdk-ruby/commit/880a6b9ec0482540da338cc3afb6fdb82eca887f))
|
|
10
|
+
|
|
3
11
|
## 1.53.0 (2026-06-30)
|
|
4
12
|
|
|
5
13
|
Full Changelog: [v1.52.0...v1.53.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.52.0...v1.53.0)
|
data/README.md
CHANGED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anthropic
|
|
4
|
+
# Tracks which fallback a sequence of requests is pinned to.
|
|
5
|
+
#
|
|
6
|
+
# Create one (`Anthropic::BetaFallbackState.new`) and pass it via the
|
|
7
|
+
# `fallback_state` request option on every request that should share the
|
|
8
|
+
# pin — the turns of one conversation, or any wider scope the stickiness
|
|
9
|
+
# should apply to; {BetaRefusalFallbackMiddleware} mutates it in place when
|
|
10
|
+
# a model refuses.
|
|
11
|
+
class BetaFallbackState
|
|
12
|
+
# Index into the fallback chain the requests are pinned to.
|
|
13
|
+
#
|
|
14
|
+
# `nil` (or `-1`) targets the original request params; the middleware sets
|
|
15
|
+
# it to the index of the fallback that accepted the request.
|
|
16
|
+
#
|
|
17
|
+
# @return [Integer, nil]
|
|
18
|
+
attr_accessor :index
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Middleware that retries refused `/v1/messages` requests down a fallback
|
|
22
|
+
# chain.
|
|
23
|
+
#
|
|
24
|
+
# Non-streaming: when a response comes back with `stop_reason: "refusal"`,
|
|
25
|
+
# the request is retried with each entry of `fallbacks` merged over the
|
|
26
|
+
# original params — passing along the refusal's `fallback_credit_token` —
|
|
27
|
+
# until a model accepts or the chain is exhausted. A message served by a
|
|
28
|
+
# fallback carries a `fallback` content block prepended at each model
|
|
29
|
+
# boundary; an exhausted chain surfaces the final refusal verbatim.
|
|
30
|
+
#
|
|
31
|
+
# Streaming: when the stream ends in `stop_reason: "refusal"`, a second
|
|
32
|
+
# request is issued to the fallback model — carrying the refused model's
|
|
33
|
+
# partial output as a trailing assistant prefill when the refusal grants one
|
|
34
|
+
# (`fallback_has_prefill_claim`), plus the refusal's `fallback_credit_token`
|
|
35
|
+
# — and the fallback's events are spliced onto the still-open stream, so the
|
|
36
|
+
# client sees one continuous message: a `fallback` content block at each
|
|
37
|
+
# model boundary, monotonic block indices, and per-hop `usage.iterations` on
|
|
38
|
+
# the final `message_delta`. Only `model` is honored from each entry on this
|
|
39
|
+
# path: the credit token is redeemable only against the refused request's
|
|
40
|
+
# body, so the other per-entry overrides would be rejected.
|
|
41
|
+
#
|
|
42
|
+
# The fallback-credit beta the credit tokens require is sent by default on
|
|
43
|
+
# every request the middleware handles; the `betas:` option controls this.
|
|
44
|
+
#
|
|
45
|
+
# To keep later requests on the model that accepted, pass a
|
|
46
|
+
# {BetaFallbackState} via the `fallback_state` request option; requests
|
|
47
|
+
# sharing that state start directly at the pinned fallback.
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# client = Anthropic::Client.new(
|
|
51
|
+
# middleware: [Anthropic::BetaRefusalFallbackMiddleware.new([{model: "claude-opus-4-8"}])]
|
|
52
|
+
# )
|
|
53
|
+
#
|
|
54
|
+
# state = Anthropic::BetaFallbackState.new
|
|
55
|
+
# message = client.beta.messages.create(**params, request_options: {fallback_state: state})
|
|
56
|
+
class BetaRefusalFallbackMiddleware
|
|
57
|
+
include Anthropic::Middleware
|
|
58
|
+
|
|
59
|
+
# Betas sent by default; override with the `betas:` option.
|
|
60
|
+
DEFAULT_BETAS = ["fallback-credit-2026-06-01"].freeze
|
|
61
|
+
|
|
62
|
+
# @param fallbacks [Array<Hash, Anthropic::Models::Beta::BetaFallbackParam>]
|
|
63
|
+
# the fallback chain, tried in order. Each entry must carry `model:`; on
|
|
64
|
+
# the non-streaming path the remaining keys (`max_tokens:`, `thinking:`,
|
|
65
|
+
# …) are merged over the original request body for that hop.
|
|
66
|
+
# @param betas [Array<String>] betas added to the `anthropic-beta` header
|
|
67
|
+
# of every `/v1/messages` request this middleware handles. Defaults to
|
|
68
|
+
# `["fallback-credit-2026-06-01"]`; pass `[]` to send none.
|
|
69
|
+
def initialize(fallbacks, betas: DEFAULT_BETAS)
|
|
70
|
+
@fallbacks = fallbacks.map { _1.respond_to?(:to_h) ? _1.to_h : _1 }.freeze
|
|
71
|
+
@betas = betas.freeze
|
|
72
|
+
@warned_missing_state = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param req [Anthropic::APIRequest]
|
|
76
|
+
# @param nxt [#call]
|
|
77
|
+
# @return [Anthropic::APIResponse]
|
|
78
|
+
def call(req, nxt)
|
|
79
|
+
return nxt.call(req) unless applies?(req)
|
|
80
|
+
|
|
81
|
+
if req.body.key?(:fallbacks)
|
|
82
|
+
raise Anthropic::Errors::Error,
|
|
83
|
+
"Sending the `fallbacks:` request param is not supported when using " \
|
|
84
|
+
"`BetaRefusalFallbackMiddleware`. Either remove the middleware and send " \
|
|
85
|
+
"`fallbacks:` with the `server-side-fallback-2026-06-01` beta header to " \
|
|
86
|
+
"let the API handle refusal fallbacks, or omit `fallbacks:` to let the " \
|
|
87
|
+
"middleware handle them on the client side."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
req = with_middleware_headers(req)
|
|
91
|
+
body = strip_fallback_blocks(req.body)
|
|
92
|
+
state = req.options[:fallback_state]
|
|
93
|
+
|
|
94
|
+
start_index = state&.index || -1
|
|
95
|
+
unless start_index.is_a?(Integer) && start_index >= -1 && start_index < @fallbacks.length
|
|
96
|
+
raise Anthropic::Errors::Error,
|
|
97
|
+
"fallback_state.index #{start_index} is out of bounds for a chain of " \
|
|
98
|
+
"#{@fallbacks.length} fallback(s); was the state shared with a different middleware?"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
pin = lambda do |index|
|
|
102
|
+
if state
|
|
103
|
+
state.index = index
|
|
104
|
+
elsif !@warned_missing_state
|
|
105
|
+
@warned_missing_state = true
|
|
106
|
+
warn(
|
|
107
|
+
"anthropic-sdk: BetaRefusalFallbackMiddleware fell back without a `fallback_state` " \
|
|
108
|
+
"request option; follow-up requests will retry models that already refused. Pass a " \
|
|
109
|
+
"shared `request_options: {fallback_state: Anthropic::BetaFallbackState.new}` to pin " \
|
|
110
|
+
"them to the accepted model."
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
streaming = req.streaming?
|
|
116
|
+
initial_body =
|
|
117
|
+
if start_index == -1
|
|
118
|
+
body
|
|
119
|
+
elsif streaming
|
|
120
|
+
# Only `model` is honored on the streaming path so the credit token
|
|
121
|
+
# — redeemable only against the refused request's body — stays valid
|
|
122
|
+
# for the spliced hops.
|
|
123
|
+
body.merge(model: @fallbacks.fetch(start_index).fetch(:model))
|
|
124
|
+
else
|
|
125
|
+
body.merge(@fallbacks.fetch(start_index))
|
|
126
|
+
end
|
|
127
|
+
res = nxt.call(req.with(body: initial_body))
|
|
128
|
+
return res unless res.status < 300
|
|
129
|
+
|
|
130
|
+
if streaming
|
|
131
|
+
first_hop = start_index + 1
|
|
132
|
+
return res unless first_hop < @fallbacks.length
|
|
133
|
+
return splice_fallback_stream(req, res, nxt, body, first_hop, pin)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
handle_non_streaming(req, res, nxt, body, start_index, pin)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# @return [Boolean] whether `req` is a `client.beta.messages` create call
|
|
142
|
+
# with a `Hash` body and a non-empty fallback chain.
|
|
143
|
+
def applies?(req)
|
|
144
|
+
return false if @fallbacks.empty?
|
|
145
|
+
return false unless req.method == :post
|
|
146
|
+
return false unless req.body.is_a?(Hash)
|
|
147
|
+
return false unless req.url.path&.end_with?("/v1/messages")
|
|
148
|
+
query = URI.decode_www_form(req.url.query.to_s).to_h
|
|
149
|
+
query["beta"] == "true"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [Anthropic::APIRequest] `req` with `@betas` appended to the
|
|
153
|
+
# `anthropic-beta` header (skipping values already present) and the
|
|
154
|
+
# fallback helper tag appended to `x-stainless-helper` — every request
|
|
155
|
+
# the middleware handles, original and each hop, reuses these headers.
|
|
156
|
+
def with_middleware_headers(req)
|
|
157
|
+
headers = {
|
|
158
|
+
Helpers::StainlessHelperHeader::HEADER =>
|
|
159
|
+
Helpers::StainlessHelperHeader.merged_value(
|
|
160
|
+
req.headers,
|
|
161
|
+
Helpers::StainlessHelperHeader::FALLBACK_REFUSAL_MIDDLEWARE
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
unless @betas.empty?
|
|
165
|
+
existing = req.headers["anthropic-beta"].to_s.split(",").map(&:strip)
|
|
166
|
+
headers["anthropic-beta"] = (existing + @betas).reject(&:empty?).uniq.join(",")
|
|
167
|
+
end
|
|
168
|
+
req.with(headers: req.headers.merge(headers))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Remove `fallback` blocks replayed in history. They only parse under the
|
|
172
|
+
# server-side fallback beta, which this middleware never sends, so a
|
|
173
|
+
# request replaying them would 400. An assistant turn left empty is dropped
|
|
174
|
+
# whole.
|
|
175
|
+
#
|
|
176
|
+
# @param body [Hash{Symbol=>Object}]
|
|
177
|
+
# @return [Hash{Symbol=>Object}]
|
|
178
|
+
def strip_fallback_blocks(body)
|
|
179
|
+
messages = body.fetch(:messages, []).filter_map do |msg|
|
|
180
|
+
content = msg[:content]
|
|
181
|
+
next msg unless content.is_a?(Array)
|
|
182
|
+
filtered = content.reject do |b|
|
|
183
|
+
type = b.is_a?(Hash) ? (b[:type] || b["type"]) : b.type
|
|
184
|
+
type.to_s == "fallback"
|
|
185
|
+
end
|
|
186
|
+
next nil if filtered.empty?
|
|
187
|
+
msg.merge(content: filtered)
|
|
188
|
+
end
|
|
189
|
+
body.merge(messages: messages)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# --- non-streaming ------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
# @param req [Anthropic::APIRequest]
|
|
195
|
+
# @param res [Anthropic::APIResponse]
|
|
196
|
+
# @param nxt [#call]
|
|
197
|
+
# @param body [Hash{Symbol=>Object}] the stripped request body
|
|
198
|
+
# @param start_index [Integer]
|
|
199
|
+
# @param pin [Proc]
|
|
200
|
+
# @return [Anthropic::APIResponse]
|
|
201
|
+
def handle_non_streaming(req, res, nxt, body, start_index, pin)
|
|
202
|
+
index = start_index
|
|
203
|
+
from_model = (start_index == -1 ? body : @fallbacks.fetch(start_index))[:model]
|
|
204
|
+
fallback_blocks = []
|
|
205
|
+
last_refusal = res
|
|
206
|
+
|
|
207
|
+
while index < @fallbacks.length - 1
|
|
208
|
+
message = decode_message(res)
|
|
209
|
+
break unless message && message["type"] == "message" && message["stop_reason"] == "refusal"
|
|
210
|
+
last_refusal = res
|
|
211
|
+
token = message.dig("stop_details", "fallback_credit_token")
|
|
212
|
+
|
|
213
|
+
index += 1
|
|
214
|
+
entry = @fallbacks.fetch(index)
|
|
215
|
+
hop_body = body.merge(entry)
|
|
216
|
+
hop_body = hop_body.merge(fallback_credit_token: token) if token
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
hop_res = nxt.call(req.with(body: hop_body))
|
|
220
|
+
rescue StandardError
|
|
221
|
+
hop_res = nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if hop_res.nil? || hop_res.status >= 300
|
|
225
|
+
next if index < @fallbacks.length - 1
|
|
226
|
+
return last_refusal
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
fallback_blocks << {
|
|
230
|
+
type: "fallback",
|
|
231
|
+
from: {model: (from_model || message["model"]).to_s},
|
|
232
|
+
to: {model: entry.fetch(:model).to_s}
|
|
233
|
+
}
|
|
234
|
+
from_model = entry.fetch(:model)
|
|
235
|
+
res = hop_res
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
return res if fallback_blocks.empty?
|
|
239
|
+
|
|
240
|
+
served = decode_message(res)
|
|
241
|
+
if served.nil? || served["type"] != "message" || served["stop_reason"] == "refusal" ||
|
|
242
|
+
!served["content"].is_a?(Array)
|
|
243
|
+
return res
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
pin.call(index)
|
|
247
|
+
served = served.merge("content" => fallback_blocks + served["content"])
|
|
248
|
+
Anthropic::APIResponse.new(
|
|
249
|
+
status: res.status,
|
|
250
|
+
headers: res.headers.merge("content-type" => "application/json").except("content-length"),
|
|
251
|
+
body: JSON.generate(served),
|
|
252
|
+
raw: res.raw,
|
|
253
|
+
request: res.request
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# @param res [Anthropic::APIResponse]
|
|
258
|
+
# @return [Hash{String=>Object}, nil] the JSON body decoded with String
|
|
259
|
+
# keys, or `nil` on a non-JSON or unparseable body.
|
|
260
|
+
def decode_message(res)
|
|
261
|
+
res.buffer!
|
|
262
|
+
JSON.parse(res.body.to_a.join)
|
|
263
|
+
rescue JSON::ParserError
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# --- streaming ----------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
# Wrap stream A in a response whose body passes events through until a
|
|
270
|
+
# retryable refusal, then splices the fallback chain's events on.
|
|
271
|
+
#
|
|
272
|
+
# @param req [Anthropic::APIRequest]
|
|
273
|
+
# @param res [Anthropic::APIResponse]
|
|
274
|
+
# @param nxt [#call]
|
|
275
|
+
# @param body [Hash{Symbol=>Object}] the stripped request body
|
|
276
|
+
# @param first_hop [Integer]
|
|
277
|
+
# @param pin [Proc]
|
|
278
|
+
# @return [Anthropic::APIResponse]
|
|
279
|
+
def splice_fallback_stream(req, res, nxt, body, first_hop, pin)
|
|
280
|
+
active_hop = [nil]
|
|
281
|
+
res.wrap_body do |upstream|
|
|
282
|
+
iter = Enumerator.new do |y|
|
|
283
|
+
spliced_events(y, upstream, req, nxt, body, first_hop, pin, active_hop)
|
|
284
|
+
end
|
|
285
|
+
Anthropic::Internal::Util.fused_enum(iter) do
|
|
286
|
+
Anthropic::Internal::Util.close_fused!(active_hop[0])
|
|
287
|
+
Anthropic::Internal::Util.close_fused!(upstream)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# rubocop:disable Metrics/BlockLength, Lint/NonLocalExitFromIterator
|
|
293
|
+
|
|
294
|
+
# @param y [Enumerator::Yielder]
|
|
295
|
+
def spliced_events(y, upstream, req, nxt, body, first_hop, pin, active_hop)
|
|
296
|
+
a = consume_hop(y, upstream, index_base: 0, has_next: true, splice: nil)
|
|
297
|
+
return unless a[:refused]
|
|
298
|
+
Anthropic::Internal::Util.close_fused!(upstream)
|
|
299
|
+
|
|
300
|
+
next_index = a[:next_index]
|
|
301
|
+
token = a.dig(:refused, :token)
|
|
302
|
+
base = []
|
|
303
|
+
partial = a.dig(:refused, :has_prefill_claim) ? to_prefill_blocks(a[:blocks]) : []
|
|
304
|
+
from_model = a[:model] || ""
|
|
305
|
+
last_usage = a.dig(:refused, :usage)
|
|
306
|
+
refusal_details = a.dig(:refused, :stop_details)
|
|
307
|
+
|
|
308
|
+
iterations = [iteration_usage("message", a[:model] || "", a.dig(:refused, :usage))]
|
|
309
|
+
|
|
310
|
+
(first_hop...@fallbacks.length).each do |hop|
|
|
311
|
+
model = @fallbacks.fetch(hop).fetch(:model).to_s
|
|
312
|
+
has_next = hop + 1 < @fallbacks.length
|
|
313
|
+
|
|
314
|
+
res_b, sent, failed = issue_hop(req, nxt, body, model, token, base, partial)
|
|
315
|
+
|
|
316
|
+
if failed
|
|
317
|
+
next if has_next
|
|
318
|
+
stop_details = refusal_details.merge("recommended_model" => model)
|
|
319
|
+
y << emit(
|
|
320
|
+
"message_delta",
|
|
321
|
+
{
|
|
322
|
+
type: "message_delta",
|
|
323
|
+
context_management: nil,
|
|
324
|
+
delta: {
|
|
325
|
+
stop_reason: "refusal",
|
|
326
|
+
stop_sequence: nil,
|
|
327
|
+
container: nil,
|
|
328
|
+
stop_details: stop_details
|
|
329
|
+
},
|
|
330
|
+
usage: last_usage || {}
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
y << emit("message_stop", {type: "message_stop"})
|
|
334
|
+
return
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
fb_index = next_index
|
|
338
|
+
next_index += 1
|
|
339
|
+
y << emit(
|
|
340
|
+
"content_block_start",
|
|
341
|
+
{
|
|
342
|
+
type: "content_block_start",
|
|
343
|
+
index: fb_index,
|
|
344
|
+
content_block: {type: "fallback", from: {model: from_model}, to: {model: model}}
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
y << emit("content_block_stop", {type: "content_block_stop", index: fb_index})
|
|
348
|
+
|
|
349
|
+
active_hop[0] = res_b.body
|
|
350
|
+
b = consume_hop(
|
|
351
|
+
y,
|
|
352
|
+
res_b.body,
|
|
353
|
+
index_base: next_index,
|
|
354
|
+
has_next: has_next,
|
|
355
|
+
splice: {iterations: iterations, model: model}
|
|
356
|
+
)
|
|
357
|
+
unless b[:refused]
|
|
358
|
+
active_hop[0] = nil
|
|
359
|
+
pin.call(hop) if b[:accepted]
|
|
360
|
+
return
|
|
361
|
+
end
|
|
362
|
+
Anthropic::Internal::Util.close_fused!(res_b.body)
|
|
363
|
+
active_hop[0] = nil
|
|
364
|
+
|
|
365
|
+
token = b.dig(:refused, :token)
|
|
366
|
+
refusal_details = b.dig(:refused, :stop_details)
|
|
367
|
+
base = sent
|
|
368
|
+
partial = b.dig(:refused, :has_prefill_claim) ? to_prefill_blocks(b[:blocks]) : []
|
|
369
|
+
iterations << iteration_usage("message", model, b.dig(:refused, :usage))
|
|
370
|
+
last_usage = b.dig(:refused, :usage)
|
|
371
|
+
from_model = model
|
|
372
|
+
next_index = b[:next_index]
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Issue one fallback hop with a single 400-on-prefill retry.
|
|
377
|
+
#
|
|
378
|
+
# @return [Array(Anthropic::APIResponse, Array<Hash>, false), Array(nil, Array<Hash>, true)]
|
|
379
|
+
# the response, the continuation actually sent (`base + partial`, or
|
|
380
|
+
# `base` if the prefill was dropped on retry), and whether the hop failed.
|
|
381
|
+
def issue_hop(req, nxt, body, model, token, base, partial)
|
|
382
|
+
continuation = base + partial
|
|
383
|
+
2.times do |attempt|
|
|
384
|
+
hop_body = body.merge(model: model, fallback_credit_token: token)
|
|
385
|
+
unless continuation.empty?
|
|
386
|
+
hop_body = hop_body.merge(
|
|
387
|
+
messages: body.fetch(:messages) + [{role: :assistant, content: continuation}]
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
begin
|
|
392
|
+
res = nxt.call(req.with(body: hop_body))
|
|
393
|
+
rescue StandardError
|
|
394
|
+
return [nil, continuation, true]
|
|
395
|
+
end
|
|
396
|
+
return [res, continuation, false] if res.status < 300
|
|
397
|
+
|
|
398
|
+
if attempt.zero? && res.status == 400 && !partial.empty?
|
|
399
|
+
err_body =
|
|
400
|
+
begin
|
|
401
|
+
JSON.parse(res.buffer!(force: true).body.to_a.join)
|
|
402
|
+
rescue JSON::ParserError, ArgumentError
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
warn(
|
|
406
|
+
"anthropic-sdk: BetaRefusalFallbackMiddleware: fallback request with the partial " \
|
|
407
|
+
"output appended was rejected (HTTP 400: #{JSON.generate(err_body)}); retrying without it"
|
|
408
|
+
)
|
|
409
|
+
continuation = base
|
|
410
|
+
partial = []
|
|
411
|
+
next
|
|
412
|
+
end
|
|
413
|
+
return [nil, continuation, true]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Consume one hop's SSE events, forwarding them to `y` while accumulating
|
|
418
|
+
# its content blocks. See the TypeScript SDK's `consumeHop` for the full
|
|
419
|
+
# contract.
|
|
420
|
+
#
|
|
421
|
+
# @param y [Enumerator::Yielder]
|
|
422
|
+
# @param upstream [Enumerable<String>]
|
|
423
|
+
# @param index_base [Integer]
|
|
424
|
+
# @param has_next [Boolean]
|
|
425
|
+
# @param splice [Hash, nil]
|
|
426
|
+
# @return [Hash{Symbol=>Object}]
|
|
427
|
+
def consume_hop(y, upstream, index_base:, has_next:, splice:)
|
|
428
|
+
tracker = BlockTracker.new(index_base)
|
|
429
|
+
model = nil
|
|
430
|
+
start_usage = nil
|
|
431
|
+
accepted = true
|
|
432
|
+
|
|
433
|
+
lines = Anthropic::Internal::Util.decode_lines(upstream)
|
|
434
|
+
Anthropic::Internal::Util.decode_sse(lines).each do |sse|
|
|
435
|
+
data = sse[:data]
|
|
436
|
+
p = data && JSON.parse(data) rescue nil # rubocop:disable Style/RescueModifier
|
|
437
|
+
|
|
438
|
+
case p && p["type"]
|
|
439
|
+
when "message_start"
|
|
440
|
+
model = p.dig("message", "model")
|
|
441
|
+
start_usage = p.dig("message", "usage")
|
|
442
|
+
next if splice
|
|
443
|
+
when "content_block_start"
|
|
444
|
+
tracker.start(p)
|
|
445
|
+
(y << emit(p["type"], p)) && next if splice
|
|
446
|
+
when "content_block_delta"
|
|
447
|
+
tracker.delta(p)
|
|
448
|
+
(y << emit(p["type"], p)) && next if splice
|
|
449
|
+
when "content_block_stop"
|
|
450
|
+
tracker.stop(p)
|
|
451
|
+
(y << emit(p["type"], p)) && next if splice
|
|
452
|
+
when "message_delta"
|
|
453
|
+
refused = p.dig("delta", "stop_reason") == "refusal"
|
|
454
|
+
accepted = false if refused
|
|
455
|
+
if refused
|
|
456
|
+
details = p.dig("delta", "stop_details")
|
|
457
|
+
details = nil unless details.is_a?(Hash) && details["type"] == "refusal"
|
|
458
|
+
if details && details["fallback_credit_token"] && has_next
|
|
459
|
+
usage = backfill(p["usage"], start_usage)
|
|
460
|
+
tracker.close_open_blocks(y)
|
|
461
|
+
return {
|
|
462
|
+
refused: {
|
|
463
|
+
token: details["fallback_credit_token"],
|
|
464
|
+
has_prefill_claim: details["fallback_has_prefill_claim"] == true,
|
|
465
|
+
usage: usage,
|
|
466
|
+
stop_details: details
|
|
467
|
+
},
|
|
468
|
+
model: model,
|
|
469
|
+
blocks: tracker.content_blocks,
|
|
470
|
+
next_index: tracker.next_index
|
|
471
|
+
}
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
if splice
|
|
475
|
+
usage = backfill(p["usage"], start_usage)
|
|
476
|
+
iter_type = refused ? "message" : "fallback_message"
|
|
477
|
+
usage["iterations"] = splice[:iterations] + [iteration_usage(iter_type, splice[:model], usage)]
|
|
478
|
+
p["usage"] = usage
|
|
479
|
+
y << emit("message_delta", p)
|
|
480
|
+
next
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
y << passthrough_sse(sse)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
{
|
|
488
|
+
refused: nil,
|
|
489
|
+
accepted: accepted,
|
|
490
|
+
model: model,
|
|
491
|
+
blocks: tracker.content_blocks,
|
|
492
|
+
next_index: tracker.next_index
|
|
493
|
+
}
|
|
494
|
+
end
|
|
495
|
+
# rubocop:enable Metrics/BlockLength, Lint/NonLocalExitFromIterator
|
|
496
|
+
|
|
497
|
+
# @param blocks [Array<Hash>]
|
|
498
|
+
# @return [Array<Hash>]
|
|
499
|
+
def to_prefill_blocks(blocks)
|
|
500
|
+
blocks.map do |b|
|
|
501
|
+
next b unless b.key?("_partial_json")
|
|
502
|
+
partial = b.delete("_partial_json")
|
|
503
|
+
b["input"] = JSON.parse(partial) rescue b["input"] # rubocop:disable Style/RescueModifier
|
|
504
|
+
b
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# @return [Hash{String=>Object}]
|
|
509
|
+
def iteration_usage(type, model, u)
|
|
510
|
+
u ||= {}
|
|
511
|
+
{
|
|
512
|
+
"type" => type,
|
|
513
|
+
"model" => model,
|
|
514
|
+
"input_tokens" => u["input_tokens"] || 0,
|
|
515
|
+
"output_tokens" => u["output_tokens"] || 0,
|
|
516
|
+
"cache_read_input_tokens" => u["cache_read_input_tokens"] || 0,
|
|
517
|
+
"cache_creation_input_tokens" => u["cache_creation_input_tokens"] || 0,
|
|
518
|
+
"cache_creation" => u["cache_creation"]
|
|
519
|
+
}
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Fill `nil`/missing fields on `primary` from `fallback`.
|
|
523
|
+
#
|
|
524
|
+
# @return [Hash{String=>Object}]
|
|
525
|
+
def backfill(primary, fallback)
|
|
526
|
+
out = (fallback || {}).merge(primary || {})
|
|
527
|
+
out.each { |k, v| out[k] = fallback[k] if v.nil? && fallback&.key?(k) }
|
|
528
|
+
out
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# @return [String] the SSE wire bytes for `payload`.
|
|
532
|
+
def emit(event, payload)
|
|
533
|
+
"event: #{event}\ndata: #{JSON.generate(payload)}\n\n"
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Forward a decoded event in (close to) its original wire bytes. SSE
|
|
537
|
+
# `id:`/`retry:` fields and comment lines are not preserved — the decoder
|
|
538
|
+
# in `Util.decode_sse` does not surface raw lines — but `event:` and the
|
|
539
|
+
# full multi-line `data:` payload are.
|
|
540
|
+
#
|
|
541
|
+
# @param sse [Hash{Symbol=>Object}]
|
|
542
|
+
# @return [String]
|
|
543
|
+
def passthrough_sse(sse)
|
|
544
|
+
out = +""
|
|
545
|
+
out << "event: #{sse[:event]}\n" if sse[:event]
|
|
546
|
+
sse[:data].to_s.chomp.split("\n", -1).each { out << "data: #{_1}\n" } if sse[:data]
|
|
547
|
+
out << "id: #{sse[:id]}\n" if sse[:id]
|
|
548
|
+
out << "retry: #{sse[:retry]}\n" if sse[:retry]
|
|
549
|
+
out << "\n"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Block bookkeeping for one stream of the splice: accumulates each content
|
|
553
|
+
# block from its deltas (for the continuation prefill), shifts wire indices
|
|
554
|
+
# by `index_base` so they stay monotonic across hops, and tracks which
|
|
555
|
+
# blocks are still open so a refusal that cuts mid-block can close them.
|
|
556
|
+
class BlockTracker
|
|
557
|
+
# @return [Integer] one past the highest shifted block index seen
|
|
558
|
+
attr_reader :next_index
|
|
559
|
+
|
|
560
|
+
# @param index_base [Integer]
|
|
561
|
+
def initialize(index_base = 0)
|
|
562
|
+
@index_base = index_base
|
|
563
|
+
@next_index = index_base
|
|
564
|
+
@blocks = []
|
|
565
|
+
@open = []
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# @return [Array<Hash>] the accumulated content blocks, in start order
|
|
569
|
+
def content_blocks = @blocks.map { _1[:block] }
|
|
570
|
+
|
|
571
|
+
# @param event [Hash]
|
|
572
|
+
def start(event)
|
|
573
|
+
@blocks << {index: event["index"], block: deep_dup(event["content_block"])}
|
|
574
|
+
event["index"] += @index_base
|
|
575
|
+
@open << event["index"]
|
|
576
|
+
@next_index = [@next_index, event["index"] + 1].max
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# @param event [Hash]
|
|
580
|
+
def delta(event)
|
|
581
|
+
apply_delta(event["index"], event["delta"])
|
|
582
|
+
event["index"] += @index_base
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# @param event [Hash]
|
|
586
|
+
def stop(event)
|
|
587
|
+
event["index"] += @index_base
|
|
588
|
+
@open.delete(event["index"])
|
|
589
|
+
@next_index = [@next_index, event["index"] + 1].max
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# @param y [Enumerator::Yielder]
|
|
593
|
+
def close_open_blocks(y)
|
|
594
|
+
@open.each do |index|
|
|
595
|
+
y << "event: content_block_stop\ndata: #{JSON.generate(
|
|
596
|
+
{
|
|
597
|
+
type: 'content_block_stop',
|
|
598
|
+
index: index
|
|
599
|
+
}
|
|
600
|
+
)}\n\n"
|
|
601
|
+
end
|
|
602
|
+
@open.clear
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
private
|
|
606
|
+
|
|
607
|
+
def apply_delta(index, delta)
|
|
608
|
+
block = @blocks.find { _1[:index] == index }&.fetch(:block)
|
|
609
|
+
return unless block
|
|
610
|
+
case delta["type"]
|
|
611
|
+
when "text_delta"
|
|
612
|
+
block["text"] = (block["text"] || +"") << delta["text"]
|
|
613
|
+
when "input_json_delta"
|
|
614
|
+
block["_partial_json"] = (block["_partial_json"] || +"") << delta["partial_json"]
|
|
615
|
+
when "citations_delta"
|
|
616
|
+
(block["citations"] ||= []) << delta["citation"]
|
|
617
|
+
when "thinking_delta"
|
|
618
|
+
block["thinking"] = (block["thinking"] || +"") << delta["thinking"]
|
|
619
|
+
when "signature_delta"
|
|
620
|
+
block["signature"] = delta["signature"]
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def deep_dup(obj)
|
|
625
|
+
case obj
|
|
626
|
+
when Hash then obj.transform_values { deep_dup(_1) }
|
|
627
|
+
when Array then obj.map { deep_dup(_1) }
|
|
628
|
+
else obj
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
@@ -17,6 +17,7 @@ module Anthropic
|
|
|
17
17
|
# telemetry consumers match on them, so renames lose history. New tags are
|
|
18
18
|
# hyphenated lowercase.
|
|
19
19
|
BETA_TOOL_RUNNER = "BetaToolRunner"
|
|
20
|
+
FALLBACK_REFUSAL_MIDDLEWARE = "fallback-refusal-middleware"
|
|
20
21
|
|
|
21
22
|
class << self
|
|
22
23
|
# The {HEADER} value to set after appending `value` to whatever is
|
|
@@ -74,6 +74,15 @@ module Anthropic
|
|
|
74
74
|
# @return [Array<#call>, #call, nil]
|
|
75
75
|
optional :middleware, Anthropic::Internal::Type::Unknown
|
|
76
76
|
|
|
77
|
+
# @!attribute fallback_state
|
|
78
|
+
# Shared {Anthropic::BetaFallbackState} for
|
|
79
|
+
# {Anthropic::BetaRefusalFallbackMiddleware}. Requests that pass the same
|
|
80
|
+
# instance start at the fallback the previous refusal pinned, so a
|
|
81
|
+
# conversation stays on the model that accepted.
|
|
82
|
+
#
|
|
83
|
+
# @return [Anthropic::BetaFallbackState, nil]
|
|
84
|
+
optional :fallback_state, Anthropic::Internal::Type::Unknown
|
|
85
|
+
|
|
77
86
|
# @!method initialize(values = {})
|
|
78
87
|
# Returns a new instance of RequestOptions.
|
|
79
88
|
#
|
data/lib/anthropic/version.rb
CHANGED
data/lib/anthropic.rb
CHANGED
|
@@ -88,6 +88,7 @@ require_relative "anthropic/helpers/input_schema/union_of"
|
|
|
88
88
|
require_relative "anthropic/helpers/input_schema/parsed_json"
|
|
89
89
|
require_relative "anthropic/input_schema"
|
|
90
90
|
require_relative "anthropic/helpers/messages"
|
|
91
|
+
require_relative "anthropic/helpers/refusal_fallback"
|
|
91
92
|
require_relative "anthropic/internal/stream"
|
|
92
93
|
require_relative "anthropic/internal/jsonl_stream"
|
|
93
94
|
require_relative "anthropic/internal/bidirectional_page_cursor"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
|
|
3
|
+
module Anthropic
|
|
4
|
+
# Tracks which fallback a sequence of requests is pinned to.
|
|
5
|
+
#
|
|
6
|
+
# Create one and pass it via the `fallback_state` request option on every
|
|
7
|
+
# request that should share the pin; {BetaRefusalFallbackMiddleware} mutates
|
|
8
|
+
# it in place when a model refuses.
|
|
9
|
+
class BetaFallbackState
|
|
10
|
+
# Index into the fallback chain the requests are pinned to. `nil`/`-1`
|
|
11
|
+
# targets the original request params.
|
|
12
|
+
sig { returns(T.nilable(Integer)) }
|
|
13
|
+
attr_accessor :index
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Middleware that retries refused `/v1/messages` requests down a fallback
|
|
17
|
+
# chain. See {BetaRefusalFallbackMiddleware} in the gem source for the full
|
|
18
|
+
# contract.
|
|
19
|
+
class BetaRefusalFallbackMiddleware
|
|
20
|
+
include Anthropic::Middleware
|
|
21
|
+
|
|
22
|
+
DEFAULT_BETAS = T.let(T.unsafe(nil), T::Array[String])
|
|
23
|
+
|
|
24
|
+
sig do
|
|
25
|
+
params(
|
|
26
|
+
fallbacks: T::Array[
|
|
27
|
+
T.any(Anthropic::Models::Beta::BetaFallbackParam, T::Hash[Symbol, T.anything])
|
|
28
|
+
],
|
|
29
|
+
betas: T::Array[String]
|
|
30
|
+
).void
|
|
31
|
+
end
|
|
32
|
+
def initialize(fallbacks, betas: DEFAULT_BETAS)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig do
|
|
36
|
+
override
|
|
37
|
+
.params(
|
|
38
|
+
request: Anthropic::APIRequest,
|
|
39
|
+
nxt: T.proc.params(req: Anthropic::APIRequest).returns(Anthropic::APIResponse)
|
|
40
|
+
)
|
|
41
|
+
.returns(Anthropic::APIResponse)
|
|
42
|
+
end
|
|
43
|
+
def call(request, nxt)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -54,6 +54,12 @@ module Anthropic
|
|
|
54
54
|
sig { returns(T.nilable(Anthropic::Middleware::EntryOrArray)) }
|
|
55
55
|
attr_accessor :middleware
|
|
56
56
|
|
|
57
|
+
# Shared {Anthropic::BetaFallbackState} for
|
|
58
|
+
# {Anthropic::BetaRefusalFallbackMiddleware}. Requests that pass the same
|
|
59
|
+
# instance start at the fallback the previous refusal pinned.
|
|
60
|
+
sig { returns(T.nilable(Anthropic::BetaFallbackState)) }
|
|
61
|
+
attr_accessor :fallback_state
|
|
62
|
+
|
|
57
63
|
# Returns a new instance of RequestOptions.
|
|
58
64
|
sig do
|
|
59
65
|
params(values: Anthropic::Internal::AnyHash).returns(T.attached_class)
|
|
@@ -7,6 +7,8 @@ module Anthropic
|
|
|
7
7
|
|
|
8
8
|
BETA_TOOL_RUNNER = T.let("BetaToolRunner", String)
|
|
9
9
|
|
|
10
|
+
FALLBACK_REFUSAL_MIDDLEWARE = T.let("fallback-refusal-middleware", String)
|
|
11
|
+
|
|
10
12
|
class << self
|
|
11
13
|
sig { params(headers: T::Hash[T.any(String, Symbol), T.untyped], value: String).returns(String) }
|
|
12
14
|
def merged_value(headers, value)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Anthropic
|
|
2
|
+
class BetaFallbackState
|
|
3
|
+
attr_accessor index: Integer?
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class BetaRefusalFallbackMiddleware
|
|
7
|
+
include Anthropic::Middleware
|
|
8
|
+
|
|
9
|
+
DEFAULT_BETAS: ::Array[String]
|
|
10
|
+
|
|
11
|
+
type fallback_entry =
|
|
12
|
+
Anthropic::Models::Beta::BetaFallbackParam | ::Hash[Symbol, top]
|
|
13
|
+
|
|
14
|
+
def initialize: (
|
|
15
|
+
::Array[fallback_entry] fallbacks,
|
|
16
|
+
?betas: ::Array[String]
|
|
17
|
+
) -> void
|
|
18
|
+
|
|
19
|
+
def call: (
|
|
20
|
+
Anthropic::APIRequest request,
|
|
21
|
+
^(Anthropic::APIRequest) -> Anthropic::APIResponse nxt
|
|
22
|
+
) -> Anthropic::APIResponse
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -10,7 +10,8 @@ module Anthropic
|
|
|
10
10
|
extra_body: top?,
|
|
11
11
|
max_retries: Integer?,
|
|
12
12
|
timeout: Float?,
|
|
13
|
-
middleware: Anthropic::Middleware::entry_or_array
|
|
13
|
+
middleware: Anthropic::Middleware::entry_or_array?,
|
|
14
|
+
fallback_state: Anthropic::BetaFallbackState?
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
class RequestOptions < Anthropic::Internal::Type::BaseModel
|
|
@@ -30,6 +31,8 @@ module Anthropic
|
|
|
30
31
|
|
|
31
32
|
attr_accessor middleware: Anthropic::Middleware::entry_or_array?
|
|
32
33
|
|
|
34
|
+
attr_accessor fallback_state: Anthropic::BetaFallbackState?
|
|
35
|
+
|
|
33
36
|
def initialize: (
|
|
34
37
|
?Anthropic::request_options | ::Hash[Symbol, top] values
|
|
35
38
|
) -> void
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: anthropic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.54.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anthropic
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cgi
|
|
@@ -96,6 +96,7 @@ files:
|
|
|
96
96
|
- lib/anthropic/helpers/input_schema/supported_schemas.rb
|
|
97
97
|
- lib/anthropic/helpers/input_schema/union_of.rb
|
|
98
98
|
- lib/anthropic/helpers/messages.rb
|
|
99
|
+
- lib/anthropic/helpers/refusal_fallback.rb
|
|
99
100
|
- lib/anthropic/helpers/stainless_helper_header.rb
|
|
100
101
|
- lib/anthropic/helpers/streaming.rb
|
|
101
102
|
- lib/anthropic/helpers/streaming/events.rb
|
|
@@ -1094,6 +1095,7 @@ files:
|
|
|
1094
1095
|
- rbi/anthropic/helpers/input_schema/enum_of.rbi
|
|
1095
1096
|
- rbi/anthropic/helpers/input_schema/json_schema_converter.rbi
|
|
1096
1097
|
- rbi/anthropic/helpers/input_schema/union_of.rbi
|
|
1098
|
+
- rbi/anthropic/helpers/refusal_fallback.rbi
|
|
1097
1099
|
- rbi/anthropic/helpers/streaming/events.rbi
|
|
1098
1100
|
- rbi/anthropic/helpers/streaming/message_stream.rbi
|
|
1099
1101
|
- rbi/anthropic/helpers/tools.rbi
|
|
@@ -2075,6 +2077,7 @@ files:
|
|
|
2075
2077
|
- sig/anthropic/errors.rbs
|
|
2076
2078
|
- sig/anthropic/file_part.rbs
|
|
2077
2079
|
- sig/anthropic/helpers/bedrock/client.rbs
|
|
2080
|
+
- sig/anthropic/helpers/refusal_fallback.rbs
|
|
2078
2081
|
- sig/anthropic/helpers/streaming/events.rbs
|
|
2079
2082
|
- sig/anthropic/helpers/streaming/message_stream.rbs
|
|
2080
2083
|
- sig/anthropic/helpers/vertex/client.rbs
|