fix: strip x-stainless-* telemetry headers from provider requests - #9401
Open
shentry wants to merge 1 commit into
Open
fix: strip x-stainless-* telemetry headers from provider requests#9401shentry wants to merge 1 commit into
shentry wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CleanHeadersTransportwould not have worked for proxy users. httpx resolves proxies throughmounts, andmountsoutrank an explicitly passedtransport=: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-addsx-stainless-retry-countandx-stainless-read-timeoutafter 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 inAsyncClient.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/AsyncAnthropicconstruction in the tree:openai_source(chat, incl. Azure and all subclasses)create_proxy_clientanthropic_source(+kimi_code)openai_embedding_sourceopenai_tts_api_sourcewhisper_api_sourceutils/file_extract.pybase_urlis hardcoded toapi.moonshot.cn, so no user-supplied relay is reachableWithout 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_sourcenever read the"proxy"field that its config template indefault.pyalready exposes, so that setting was silently ignored. It now works.The duplicated "find the SDK's own
httpxmodule" block (needed because the SDKsisinstance-checkhttp_clientagainst their own import) is now one helper per SDK innetwork_utils, since three more call sites needed it.Tests
Added to
tests/unit/test_network_utils.py, driving the real SDKs over aMockTransport:create_proxy_clientclient sends none, andauthorization/content-typesurvivex-api-keysurvivesstrip_sdk_telemetry_headersleaves other headers alone, and does not eat a barex-stainlesswith no trailing dashresolve_*_httpx_modulehelpers prefer the SDK module and fall back to the global oneSeven 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.pyhad 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 passedpytest tests/-> 1872 passed, 3 failedruff format --check/ruff check(0.15.22) on all eight touched files -> cleanThe 3 failures are
tests/test_dashboard.py::test_t2i_*and are pre-existing — I confirmed they fail the same way on a cleanmasterwith 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:
Enhancements:
Tests: