diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c33eb8cb..bee3a52f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.42.0"
+ ".": "2.43.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2d7509b..3cc51978 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 2.43.0 (2026-06-18)
+
+Full Changelog: [v2.42.0...v2.43.0](https://github.com/anthropics/anthropic-sdk-java/compare/v2.42.0...v2.43.0)
+
+### Features
+
+* **helpers:** introduce x-stainless-helper telemetry + tag the refusal-fallback interceptor ([#91](https://github.com/anthropics/anthropic-sdk-java/issues/91)) ([a14925b](https://github.com/anthropics/anthropic-sdk-java/commit/a14925bb5b3f1495a553e310e57705a597581b62))
+
## 2.42.0 (2026-06-18)
Full Changelog: [v2.41.1...v2.42.0](https://github.com/anthropics/anthropic-sdk-java/compare/v2.41.1...v2.42.0)
diff --git a/README.md b/README.md
index b2531e66..ded12e0b 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Full documentation is available at **[platform.claude.com/docs/en/api/sdks/java]
### Gradle
```kotlin
-implementation("com.anthropic:anthropic-java:2.42.0")
+implementation("com.anthropic:anthropic-java:2.43.0")
```
### Maven
@@ -24,7 +24,7 @@ implementation("com.anthropic:anthropic-java:2.42.0")
com.anthropic
anthropic-java
- 2.42.0
+ 2.43.0
```
diff --git a/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptor.kt b/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptor.kt
index 60f765d5..c79d6769 100644
--- a/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptor.kt
+++ b/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptor.kt
@@ -463,9 +463,10 @@ private constructor(
)
}
- // Send the configured betas on this and every hop request derived from it.
+ // Send the configured betas on this and every hop request derived from it,
+ // and tag this and every hop with the interceptor's helper telemetry.
return PreparedRequest(
- appendBetas(trimmedRequest),
+ withInterceptorHeaders(trimmedRequest),
body,
state,
index,
@@ -655,22 +656,29 @@ private constructor(
}
/**
- * Returns a copy of the request with [betas] appended to its `anthropic-beta` header, skipping
- * values already present (set by the caller or another interceptor).
+ * Returns a copy of the request with [betas] appended to its `anthropic-beta` header (skipping
+ * values already present) and the interceptor's helper-telemetry tag appended to
+ * `x-stainless-helper`. Single rebuild for both.
*/
- private fun appendBetas(request: HttpRequest): HttpRequest {
- if (betas.isEmpty()) {
- return request
- }
+ private fun withInterceptorHeaders(request: HttpRequest): HttpRequest {
+ val builder = request.toBuilder()
val existing =
request.headers.values("anthropic-beta").flatMapTo(mutableSetOf()) { value ->
value.split(",").map { it.trim() }
}
val missing = betas.map { it.asString() }.filter { existing.add(it) }
- if (missing.isEmpty()) {
- return request
+ if (missing.isNotEmpty()) {
+ builder.putHeaders("anthropic-beta", missing)
}
- return request.toBuilder().putHeaders("anthropic-beta", missing).build()
+ return builder
+ .replaceHeaders(
+ STAINLESS_HELPER_HEADER,
+ mergedStainlessHelperValue(
+ request.headers,
+ StainlessHelperHeaderValue.FALLBACK_REFUSAL_MIDDLEWARE,
+ ),
+ )
+ .build()
}
/**
diff --git a/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/StainlessHelperHeader.kt b/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/StainlessHelperHeader.kt
new file mode 100644
index 00000000..08e25a2c
--- /dev/null
+++ b/anthropic-java-core/src/main/kotlin/com/anthropic/helpers/StainlessHelperHeader.kt
@@ -0,0 +1,41 @@
+package com.anthropic.helpers
+
+import com.anthropic.core.http.Headers
+
+/**
+ * Telemetry header naming the SDK helper(s) a request came from. Always this lowercase form;
+ * [Headers] is case-insensitive but a single canonical casing keeps every call site greppable.
+ */
+@JvmSynthetic internal const val STAINLESS_HELPER_HEADER = "x-stainless-helper"
+
+/**
+ * The closed set of helper telemetry tags, shared verbatim across SDKs. A typo at any call site is
+ * a compile error rather than silently mistagged telemetry. Existing values keep their original
+ * spellings — telemetry consumers match on them, so renames lose history. New tags are hyphenated
+ * lowercase.
+ */
+internal enum class StainlessHelperHeaderValue(@JvmSynthetic internal val value: String) {
+ FALLBACK_REFUSAL_MIDDLEWARE("fallback-refusal-middleware");
+
+ override fun toString(): String = value
+}
+
+/**
+ * Returns the [STAINLESS_HELPER_HEADER] value to set after appending [value] to whatever is already
+ * present in [headers] — existing tags keep their position, the new tag goes at the end, duplicates
+ * are dropped, joined as one comma-separated string. The backend logs the header as one opaque
+ * string, so a second header line or a clobbered value loses data; callers set the result via
+ * `replaceHeaders(STAINLESS_HELPER_HEADER, …)`.
+ */
+@JvmSynthetic
+internal fun mergedStainlessHelperValue(
+ headers: Headers,
+ value: StainlessHelperHeaderValue,
+): String {
+ val tokens = LinkedHashSet()
+ headers.values(STAINLESS_HELPER_HEADER).forEach { existing ->
+ existing.split(",").map(String::trim).filter(String::isNotEmpty).forEach(tokens::add)
+ }
+ tokens.add(value.value)
+ return tokens.joinToString(", ")
+}
diff --git a/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptorTest.kt b/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptorTest.kt
index 76c6645a..8d0c2bcd 100644
--- a/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptorTest.kt
+++ b/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/BetaRefusalFallbackInterceptorTest.kt
@@ -259,6 +259,73 @@ internal class BetaRefusalFallbackInterceptorTest {
assertThat(fallbackState.index()).isEqualTo(-1)
}
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun tagsTheOriginalAndFallbackRequests(async: Boolean) {
+ val httpClient =
+ FakeHttpClient(refusal("primary-model", "credit-token"), message("fallback-model"))
+ val interceptedClient =
+ BetaRefusalFallbackInterceptor.builder()
+ .addFallback(fallback("fallback-model"))
+ .build()
+ .intercept(httpClient)
+
+ interceptedClient.execute(
+ messagesRequest(),
+ RequestOptions.builder().fallbackState(BetaFallbackState.create()).build(),
+ async,
+ )
+
+ assertThat(httpClient.requests.map { it.headers.values(STAINLESS_HELPER_HEADER) })
+ .containsExactly(
+ listOf("fallback-refusal-middleware"),
+ listOf("fallback-refusal-middleware"),
+ )
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun appendsToAHelperTagAlreadyOnTheRequest(async: Boolean) {
+ val httpClient = FakeHttpClient(message("primary-model"))
+ val interceptedClient =
+ BetaRefusalFallbackInterceptor.builder()
+ .addFallback(fallback("fallback-model"))
+ .build()
+ .intercept(httpClient)
+
+ interceptedClient.execute(
+ messagesRequest().toBuilder().putHeader("X-Stainless-Helper", "BetaToolRunner").build(),
+ RequestOptions.builder().fallbackState(BetaFallbackState.create()).build(),
+ async,
+ )
+
+ assertThat(httpClient.requests.single().headers.values(STAINLESS_HELPER_HEADER))
+ .containsExactly("BetaToolRunner, fallback-refusal-middleware")
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun doesNotTagRequestsItPassesThrough(async: Boolean) {
+ val httpClient = FakeHttpClient(message("primary-model"))
+ val interceptedClient =
+ BetaRefusalFallbackInterceptor.builder()
+ .addFallback(fallback("fallback-model"))
+ .build()
+ .intercept(httpClient)
+
+ // the GA surface (no ?beta=true) is not applicable to this interceptor
+ val gaRequest =
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl("https://api.example.com")
+ .addPathSegments("v1", "messages")
+ .body(HttpRequestBody.ofJson(messagesBody()))
+ .build()
+ interceptedClient.execute(gaRequest, RequestOptions.none(), async)
+
+ assertThat(httpClient.requests.single().headers.values(STAINLESS_HELPER_HEADER)).isEmpty()
+ }
+
@ParameterizedTest
@ValueSource(booleans = [false, true])
fun walksEachHopThroughTheChainUntilAModelAccepts(async: Boolean) {
diff --git a/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/StainlessHelperHeaderTest.kt b/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/StainlessHelperHeaderTest.kt
new file mode 100644
index 00000000..929d428e
--- /dev/null
+++ b/anthropic-java-core/src/test/kotlin/com/anthropic/helpers/StainlessHelperHeaderTest.kt
@@ -0,0 +1,56 @@
+package com.anthropic.helpers
+
+import com.anthropic.core.http.Headers
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class StainlessHelperHeaderTest {
+
+ @Test
+ fun `appends to an empty headers`() {
+ val merged =
+ mergedStainlessHelperValue(
+ Headers.builder().build(),
+ StainlessHelperHeaderValue.FALLBACK_REFUSAL_MIDDLEWARE,
+ )
+ assertThat(merged).isEqualTo("fallback-refusal-middleware")
+ }
+
+ @Test
+ fun `appends to an existing tag`() {
+ val headers = Headers.builder().put(STAINLESS_HELPER_HEADER, "BetaToolRunner").build()
+ val merged =
+ mergedStainlessHelperValue(
+ headers,
+ StainlessHelperHeaderValue.FALLBACK_REFUSAL_MIDDLEWARE,
+ )
+ assertThat(merged).isEqualTo("BetaToolRunner, fallback-refusal-middleware")
+ }
+
+ @Test
+ fun `dedups`() {
+ val headers =
+ Headers.builder().put(STAINLESS_HELPER_HEADER, "fallback-refusal-middleware").build()
+ val merged =
+ mergedStainlessHelperValue(
+ headers,
+ StainlessHelperHeaderValue.FALLBACK_REFUSAL_MIDDLEWARE,
+ )
+ assertThat(merged).isEqualTo("fallback-refusal-middleware")
+ }
+
+ @Test
+ fun `collapses multiple lines and casings`() {
+ val headers =
+ Headers.builder()
+ .put("X-Stainless-Helper", "a")
+ .put(STAINLESS_HELPER_HEADER, "b, c")
+ .build()
+ val merged =
+ mergedStainlessHelperValue(
+ headers,
+ StainlessHelperHeaderValue.FALLBACK_REFUSAL_MIDDLEWARE,
+ )
+ assertThat(merged).isEqualTo("a, b, c, fallback-refusal-middleware")
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index d8b18b4f..135a99ad 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,4 @@
allprojects {
group = "com.anthropic"
- version = "2.42.0" // x-release-please-version
+ version = "2.43.0" // x-release-please-version
}