From 06744370080a84cffc20f9c2b9d66112b01e7f4a Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 10 Jul 2026 09:00:17 -0700 Subject: [PATCH 1/3] Support Fable 5 fallback beta header --- R/provider-claude.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/R/provider-claude.R b/R/provider-claude.R index 73fc2afa..a9a47013 100644 --- a/R/provider-claude.R +++ b/R/provider-claude.R @@ -448,6 +448,9 @@ method(value_turn, ProviderAnthropic) <- function( content$thinking, extra = list(signature = content$signature) ) + } else if (content$type == "fallback") { + # https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback + NULL } else { cli::cli_abort( "Unknown content type {.str {content$type}}.", @@ -455,6 +458,7 @@ method(value_turn, ProviderAnthropic) <- function( ) } }) + contents <- compact(contents) tokens <- value_tokens(provider, result) cache_write <- result$usage$cache_creation_input_tokens %||% 0 From 54d6f81a5734f0abef9197601f394c55d4d6e740 Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 10 Jul 2026 09:06:16 -0700 Subject: [PATCH 2/3] add NEWS entry --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 651c634d..6f360c66 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ * `Chat` gains a `set_model()` method for updating the model after chat creation. Unlike some `chat_*()` functions, the model name is not validated (#988). * `chat()` now raises a warning and `chat_structured()` raises an informative error when a response is truncated, filtered, or otherwise incomplete (@thisisnic, #867). * `chat_anthropic()` now supports `params(reasoning_effort =)` for Claude's adaptive thinking mode (@thisisnic, #987). +* `chat_anthropic()` now correctly handles the `fallback` content block returned when a model's server-side refusal fallback (`server-side-fallback-2026-06-01`) is triggered (@simonpcouch, #1058). * `chat_google_gemini()` now defaults to the `gemini-3.5-flash` model (@thisisnic, #885). * `chat_google_gemini()` and `chat_google_vertex()` now support `params(reasoning_effort =)` (@thisisnic, #873). * `chat_google_vertex()` and `models_google_vertex()` now default `location` and `project_id` to the `GOOGLE_CLOUD_LOCATION` and `GOOGLE_CLOUD_PROJECT` environment variables, no longer incorrectly use `GOOGLE_API_KEY` for authentication, and give a clearer error when cached credentials are invalid (@thisisnic, #994). From 689eee352fe38bd7ea4958433f68c1fd6e7abd1f Mon Sep 17 00:00:00 2001 From: Simon Couch Date: Fri, 10 Jul 2026 12:54:19 -0700 Subject: [PATCH 3/3] price refusal fallback at serving model's rate --- R/provider-claude.R | 20 +++++++++++- R/tokens.R | 9 +++-- tests/testthat/test-provider-claude.R | 47 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/R/provider-claude.R b/R/provider-claude.R index a9a47013..bc4535b3 100644 --- a/R/provider-claude.R +++ b/R/provider-claude.R @@ -466,7 +466,11 @@ method(value_turn, ProviderAnthropic) <- function( # already counts them at 1.0x, so add the 0.25x surcharge for pricing. cost_tokens <- tokens cost_tokens$input <- cost_tokens$input + cache_write * 0.25 - cost <- get_token_cost(provider, cost_tokens) + cost <- get_token_cost( + provider, + cost_tokens, + model = serving_model(result) %||% provider@model + ) AssistantTurn( contents, json = result, @@ -476,6 +480,20 @@ method(value_turn, ProviderAnthropic) <- function( ) } +# The model that produced the returned message. `result$model` is unreliable +# for a mid-output fallback in a stream (it keeps the requested model named at +# `message_start`), so prefer the last `fallback` block's `to.model`. +serving_model <- function(result) { + to_models <- compact(lapply(result$content, function(content) { + if (identical(content$type, "fallback")) content$to$model + })) + if (length(to_models) > 0) { + to_models[[length(to_models)]] + } else { + result$model + } +} + # ellmer -> Claude -------------------------------------------------------------- method(as_json, list(ProviderAnthropic, Turn)) <- function( diff --git a/R/tokens.R b/R/tokens.R index 7c39f1e9..09010146 100644 --- a/R/tokens.R +++ b/R/tokens.R @@ -117,12 +117,17 @@ has_cost <- function(provider, model) { vctrs::vec_in(needle, prices[c("provider", "model")]) } -get_token_cost <- function(provider, tokens, variant = "") { +get_token_cost <- function( + provider, + tokens, + variant = "", + model = provider@model +) { check_string(variant, .internal = TRUE) needle <- data.frame( provider = provider@name, - model = provider@model, + model = model, variant = variant ) idx <- vctrs::vec_match(needle, prices[c("provider", "model", "variant")]) diff --git a/tests/testthat/test-provider-claude.R b/tests/testthat/test-provider-claude.R index b93d4ae6..66d23277 100644 --- a/tests/testthat/test-provider-claude.R +++ b/tests/testthat/test-provider-claude.R @@ -256,6 +256,53 @@ test_that("value_turn() prices cache writes at 1.25x while reporting raw tokens" expect_equal(unclass(turn@cost), expected_cost) }) +test_that("value_turn() prices a refusal fallback at the serving model's rate", { + provider <- ProviderAnthropic( + name = "Anthropic", + base_url = "https://api.anthropic.com/v1", + model = "claude-fable-5", + params = list(), + extra_args = list(), + extra_headers = character(), + credentials = NULL, + beta_headers = character(), + cache = "" + ) + + result <- list( + model = "claude-opus-4-8", + content = list( + list( + type = "fallback", + from = list(model = "claude-fable-5"), + to = list(model = "claude-opus-4-8") + ), + list(type = "text", text = "ok") + ), + stop_reason = "end_turn", + usage = list(input_tokens = 1000, output_tokens = 50) + ) + + turn <- value_turn(provider, result) + + # opus-4-8 rates ($5/$25 per 1M), not fable-5's ($10/$50). + expect_equal(unclass(turn@cost), (1000 * 5 + 50 * 25) / 1e6) +}) + +test_that("serving_model() prefers the last fallback block's to.model", { + expect_equal(serving_model(list(model = "a", content = list())), "a") + expect_equal( + serving_model(list( + model = "requested", + content = list( + list(type = "fallback", to = list(model = "served")), + list(type = "text", text = "hi") + ) + )), + "served" + ) +}) + test_that("stream_merge_chunks() handles citations_delta", { provider <- ProviderAnthropic( name = "Anthropic",