Skip to content

Add an option to disable Stainless telemetry headers - #962

Open
arkzoe wants to merge 2 commits into
openai:mainfrom
arkzoe:contrib/issue-715-azure-telemetry-headers
Open

Add an option to disable Stainless telemetry headers#962
arkzoe wants to merge 2 commits into
openai:mainfrom
arkzoe:contrib/issue-715-azure-telemetry-headers

Conversation

@arkzoe

@arkzoe arkzoe commented Sep 2, 2026

Copy link
Copy Markdown

What

Add a ClientOptions.Builder.sendStainlessHeaders(boolean) option.

The option defaults to true to preserve the existing behavior. When disabled,
the SDK does not add its default X-Stainless-* telemetry headers.

Why

Azure OpenAI can reject requests with HTTP 431 when the request contains too
many custom headers. This gives users a supported workaround without changing
the default behavior.

Fixes #715

Tests

  • git diff --check
  • ./gradlew :openai-java-core:test --tests com.openai.core.ClientOptionsTest

@arkzoe
arkzoe requested a review from a team as a code owner September 2, 2026 09:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a066d1389c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
if (sendStainlessHeaders) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the switch for headers added during request execution

When this option is false, only the static headers stored in ClientOptions.headers are omitted. Every request still receives X-Stainless-Retry-Count in RetryingHttpClient.kt lines 36-45/80-92, and the default OkHttp transport adds X-Stainless-Read-Timeout and X-Stainless-Timeout in OkHttpClient.kt lines 255-269. Consequently, users near Azure's custom-header limit still send three SDK telemetry headers and can continue receiving HTTP 431; propagate the setting into these request-time injection paths and verify the headers captured on the wire.

Useful? React with 👍 / 👎.

Comment on lines +463 to +465
fun sendStainlessHeaders(sendStainlessHeaders: Boolean) = apply {
this.sendStainlessHeaders = sendStainlessHeaders
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the option through the public OkHttp builders

Azure users normally construct clients with OpenAIOkHttpClient.builder() or OpenAIOkHttpClientAsync.builder(), but neither builder delegates this new setting to its internal ClientOptions.Builder, unlike the other client options. Therefore .sendStainlessHeaders(false) is unavailable in the standard construction flow targeted by this workaround; users would have to discover the indirect post-build withOptions path instead.

Useful? React with 👍 / 👎.

}
azureServiceVersion = clientOptions.azureServiceVersion
azureUrlPathMode = clientOptions.azureUrlPathMode
sendStainlessHeaders = clientOptions.sendStainlessHeaders

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove materialized headers when disabling after cloning

When sendStainlessHeaders(false) is applied to a builder obtained from toBuilder() whose source used the default true, from() has already copied the source's materialized X-Stainless-* headers into this.headers at line 243. The subsequent build skips generating new defaults but restores those copied headers at line 739, leaving the option false while all eight static telemetry headers are still sent. This also makes the post-build withOptions workaround ineffective; remove the SDK-generated headers when disabling or keep user-supplied and generated headers separate.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05378f305b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


val client = clientBuilder.build()
return client.newCall(request.toRequest(client))
return client.newCall(request.toRequest(client, sendStainlessHeaders))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate opt-out through withOptions transport clones

When a client initially uses the default and is later cloned with client.withOptions(b -> b.sendStainlessHeaders(false)), ClientOptions.Builder.from() reuses the original HttpClient, whose new transport-level flag was fixed to true at construction. The clone omits static and retry headers, but this call still causes the reused OkHttp transport to add X-Stainless-Read-Timeout and X-Stainless-Timeout; with the nonzero default timeouts, the Azure workaround remains incomplete. Fresh evidence in this revision is that the transport now captures the flag immutably while withOptions continues reusing originalHttpClient; propagate the updated policy to cloned transports or make request-time injection consult the current options, and cover the public withOptions flow on the wire.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

.maxIdleConnections(maxIdleConnections)
.keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sendStainlessHeaders(sendStainlessHeaders)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the opt-out to X.509 transports

When sendStainlessHeaders(false) is combined with x509WorkloadIdentity(...), build() selects the x509ClientOptions branch and never reaches this only propagation into the OkHttp builder. X509Transport.bind() constructs both underlying OkHttp clients with the default true, so their requests still receive the timeout telemetry headers. Fresh evidence beyond the earlier general transport concern is this alternate X.509 construction path; pass the setting into the bound transport and add a wire-level X.509 regression test.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

Comment on lines +258 to +260
if (!sendStainlessHeaders) {
headers.removeAll(STAINLESS_HEADER_NAMES)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit Stainless headers across no-op clones

If options are built with .sendStainlessHeaders(false).putHeader("X-Stainless-Lang", "custom"), the later explicit header is retained by build(), but a no-op toBuilder().build() copies it and then removes it here solely because the saved flag is false. Consequently, withOptions for an unrelated setting can silently discard caller-provided headers. Fresh evidence in this revision is the new blanket cleanup added to address materialized defaults; distinguish generated defaults from user headers rather than deleting every matching name during cloning, and cover cloning after an explicit post-opt-out header.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI Java SDK issues with Azure OpenAI

1 participant