Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
24 changes: 23 additions & 1 deletion R/provider-claude.R
Original file line number Diff line number Diff line change
Expand Up @@ -448,21 +448,29 @@ 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}}.",
.internal = TRUE
)
}
})
contents <- compact(contents)

tokens <- value_tokens(provider, result)
cache_write <- result$usage$cache_creation_input_tokens %||% 0
# Anthropic charges 1.25x the input rate for cache writes; tokens$input
# 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,
Expand All @@ -472,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(
Expand Down
9 changes: 7 additions & 2 deletions R/tokens.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")])
Expand Down
47 changes: 47 additions & 0 deletions tests/testthat/test-provider-claude.R
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading