Skip to content

fix: strip x-stainless-* telemetry headers from provider requests - #9401

Open
shentry wants to merge 1 commit into
AstrBotDevs:masterfrom
shentry:fix/8531-strip-stainless-headers
Open

fix: strip x-stainless-* telemetry headers from provider requests#9401
shentry wants to merge 1 commit into
AstrBotDevs:masterfrom
shentry:fix/8531-strip-stainless-headers

Conversation

@shentry

@shentry shentry commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #8531

Problem

The OpenAI Python SDK is generated by Stainless, which stamps every request with telemetry headers naming the SDK version, OS, CPU architecture and Python runtime. Some OpenAI-compatible relays read that as an "official SDK" signature and answer 403 Your request was blocked, while the identical request sent by curl succeeds.

Two things the issue does not mention, both confirmed against the pinned SDKs (openai 2.48.0, anthropic 0.120.0):

The Anthropic SDK has the same problem. It is Stainless-generated too and sends ten of these headers — the nine OpenAI sends plus x-stainless-timeout. Anthropic-compatible relays are just as affected.

The proposed CleanHeadersTransport would not have worked for proxy users. httpx resolves proxies through mounts, and mounts outrank an explicitly passed transport=:

>>> c = httpx.AsyncClient(proxy="http://127.0.0.1:7890", transport=httpx.AsyncHTTPTransport())
>>> c._transport
<httpx.AsyncHTTPTransport object at 0x104339940>          # the transport we passed
>>> c._transport_for_url(httpx.URL("https://api.openai.com/v1"))
<httpx.AsyncHTTPTransport object at 0x10a8d1b50>          # the proxy mount actually used

So a header-stripping transport would be silently bypassed for everyone with a proxy configured — likely a large share of the people hitting third-party relays in the first place.

I also checked the tidier-looking default_headers={"x-stainless-lang": Omit(), ...} route. It does not work: all nine headers still go out, because the SDK re-adds x-stainless-retry-count and x-stainless-read-timeout after the merge and only suppresses them when the per-request options ask for it.

Change

Strip the headers in an httpx request event hook, installed by create_proxy_client. Event hooks run in AsyncClient.send() before transport selection, so they apply on the direct and the proxied path alike.

Matching is on the x-stainless- prefix rather than a fixed list, since the two SDKs already disagree on the exact set and Stainless keeps adding to it.

The hook only helps if a client actually exists, and most SDK call sites were not building one. Audit of every AsyncOpenAI / AsyncAnthropic construction in the tree:

call site before after
openai_source (chat, incl. Azure and all subclasses) always via create_proxy_client unchanged, now stripped
anthropic_source (+ kimi_code) client only when a proxy was set always
openai_embedding_source client only when a proxy was set always
openai_tts_api_source client only when a proxy was set always
whisper_api_source never built one always
utils/file_extract.py none left alone — base_url is hardcoded to api.moonshot.cn, so no user-supplied relay is reachable

Without this the bug would only be half fixed: a relay that blocks chat blocks embeddings and TTS on the same account.

Two incidental consequences of routing those four through create_proxy_client:

  • whisper_api_source never read the "proxy" field that its config template in default.py already exposes, so that setting was silently ignored. It now works.
  • Embedding/TTS/Whisper now get the hybrid system-store + certifi SSL context that the chat providers already use, instead of the httpx default.

The duplicated "find the SDK's own httpx module" block (needed because the SDKs isinstance-check http_client against their own import) is now one helper per SDK in network_utils, since three more call sites needed it.

Tests

Added to tests/unit/test_network_utils.py, driving the real SDKs over a MockTransport:

  • a premise guard asserting the OpenAI SDK really does send all nine headers with a plain client — so the suite fails loudly if this ever stops being true and the rest of these tests start passing vacuously
  • OpenAI SDK through a create_proxy_client client sends none, and authorization / content-type survive
  • Anthropic SDK likewise, and x-api-key survives
  • stripping still happens when the request is routed through a mounted (proxy) transport — this is the case a custom transport would have missed
  • strip_sdk_telemetry_headers leaves other headers alone, and does not eat a bare x-stainless with no trailing dash
  • the hook is registered with and without a proxy
  • both resolve_*_httpx_module helpers prefer the SDK module and fall back to the global one

Seven of the eight new tests fail against the pre-fix code (the premise guard passes either way by design; I verified this by stashing the astrbot/ changes and re-running).

tests/test_anthropic_kimi_code_provider.py had two assertions encoding the old "no client without a proxy" rule; both are updated to the new invariant rather than deleted.

  • pytest tests/unit/ -> 737 passed
  • pytest tests/ -> 1872 passed, 3 failed
  • ruff format --check / ruff check (0.15.22) on all eight touched files -> clean

The 3 failures are tests/test_dashboard.py::test_t2i_* and are pre-existing — I confirmed they fail the same way on a clean master with none of my changes applied.

Note

Stripping is unconditional. These headers are informational only, so dropping them costs nothing functionally, and it also stops AstrBot from reporting the host OS, CPU architecture and Python version to every third-party endpoint a user configures. Happy to put it behind a provider config flag instead if you would rather keep it opt-in.

Summary by Sourcery

Strip Stainless-generated telemetry headers from OpenAI and Anthropic SDK requests and ensure all provider clients consistently use proxy-aware httpx clients with this stripping applied.

Bug Fixes:

  • Prevent OpenAI- and Anthropic-compatible relays from rejecting SDK requests due to x-stainless-* telemetry headers, including for chat, embeddings, TTS, and Whisper APIs.

Enhancements:

  • Always construct shared httpx clients via create_proxy_client for Anthropic, OpenAI embedding, TTS, and Whisper providers, aligning their proxy and SSL handling with chat providers.
  • Introduce helpers to resolve the SDK-specific httpx modules for OpenAI and Anthropic to maintain compatibility with their http_client type checks.

Tests:

  • Add unit tests validating header stripping behavior for real OpenAI and Anthropic SDKs, including proxy/mount scenarios and correctness of the stripping hook registration and httpx module resolution.
  • Update Anthropic Kimi Code provider tests to reflect the new invariant that an http_client is always created, even without a proxy.

The OpenAI and Anthropic Python SDKs are both generated by Stainless,
which stamps every request with x-stainless-* headers describing the SDK
version, OS, architecture and Python runtime. Some OpenAI-compatible
relays treat those headers as an "official SDK" signature and reject the
request with HTTP 403 "Your request was blocked", while the identical
request sent by curl succeeds.

Remove them with an httpx request event hook installed by
create_proxy_client. A hook rather than a custom AsyncHTTPTransport:
httpx resolves proxies through mounts, which outrank an explicitly
passed transport=, so a header-stripping transport would be silently
bypassed for every user who configured a proxy.

Route the remaining SDK client construction through create_proxy_client
so the hook is not limited to the chat path:

- anthropic: built a client only when a proxy was set, otherwise let the
  SDK use its own default client
- openai embedding / tts: same
- whisper: never built one at all, and consequently ignored the "proxy"
  field its config template already exposes

Fixes AstrBotDevs#8531
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]OpenAI SDK 自动添加的 x-stainless-* 请求头导致部分 API 提供商返回 "Your request was blocked"

1 participant