Stainless-generated SDKs send X-Stainless-* request headers on every call — X-Stainless-Lang, X-Stainless-Package-Version, X-Stainless-OS, X-Stainless-Arch, X-Stainless-Runtime, and X-Stainless-Runtime-Version are all set automatically, based on the header-building code visible in OpenAI's Python SDK, which is Stainless-generated. If you're moving off Stainless generation, your next SDK build won't send these headers unless you add the equivalent yourself. Before cutover: pick your own header (or a structured User-Agent string), add it to whatever generates your new SDKs, and dual-read both header sets in your logs for at least one full release cycle so you don't lose visibility into who's running what.
What you actually lose
If you query your API logs today for "which SDK language and version is each customer running," and that query filters on X-Stainless-Lang or X-Stainless-Package-Version, it will silently return nothing for every customer who upgrades to your post-Stainless SDK — not an error, just an empty result for a growing share of traffic. That's the trap: the query still runs, the dashboard still renders, and the data quietly narrows to "customers who haven't upgraded yet" without anyone noticing until someone asks why version adoption looks stalled.
The headers Stainless SDKs send, based on the pattern in openai-python's platform_headers() and _build_headers() functions:
| Header | What it carries |
|---|---|
X-Stainless-Lang |
SDK language (python, typescript, etc.) |
X-Stainless-Package-Version |
The published SDK package version |
X-Stainless-OS |
Client OS |
X-Stainless-Arch |
Client architecture |
X-Stainless-Runtime |
Runtime name (e.g. CPython, Node) |
X-Stainless-Runtime-Version |
Runtime version |
X-Stainless-Retry-Count |
Retry attempt number for the current request |
X-Stainless-Read-Timeout |
The configured read timeout for the request |
Confirm this against your own SDK's source before you rely on it — header names and behavior can differ by SDK, and this list reflects one specific generated client, not a universal Stainless standard.
Replace it before cutover, not after
1. Pick your header convention
Two workable options:
- A custom header set, mirroring what you're replacing:
X-YourSDK-Lang,X-YourSDK-Version,X-YourSDK-Runtime. Easiest to query the same way your existing dashboards already do, since it's a straight header-name swap. - A structured
User-Agentstring —your-sdk-python/2.4.1 (CPython/3.12.1; Linux)— which is the more common convention outside the Stainless ecosystem and needs no new header, just parsing on the log side.
Either works. The header-set option is less migration work if your logging queries already filter on header names; the User-Agent option is more conventional if you're building fresh tooling anyway.
2. Add it to your new SDK's generation config
Wherever your new generator builds the HTTP client (a base client class, a request-wrapping function, middleware), set the header or append to User-Agent on every request, the same way the old SDK did it automatically. This is a few lines in the generator's client template, not a per-endpoint change — do it once at the client-construction level so it applies to every generated method.
3. Dual-read both header sets in your logs and dashboards
For at least one full release cycle after cutover, have your logging queries and dashboards check for either the old X-Stainless-* headers or your new ones, and merge the results. Customers upgrade on their own schedule — some will be on your last Stainless-generated SDK version for months. If you switch your queries over in one step, you undercount every customer who hasn't upgraded yet, which is exactly the "adoption looks stalled" problem in reverse.
A rough log-query pattern (adjust to your log platform):
SELECT
COALESCE(headers['x-yoursdk-lang'], headers['x-stainless-lang']) AS sdk_lang,
COALESCE(headers['x-yoursdk-version'], headers['x-stainless-package-version']) AS sdk_version,
COUNT(*) AS requests
FROM api_logs
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY 1, 2
ORDER BY requests DESC;
4. Retire the dual-read on a schedule, not indefinitely
Set a date (a quarter out is reasonable for most APIs) to drop the X-Stainless-* fallback once new-header adoption is high enough. Keeping dual-read logic forever just adds maintenance burden for a signal that will eventually go to zero.
Why this matters beyond dashboards
SDK version telemetry isn't just a nice-to-have metric — it's what tells you whether it's safe to make a breaking API change. If your logs show 40% of traffic still on an SDK version two majors behind, that's a real deprecation-timeline input, not trivia. Your SDK is three versions behind your API covers the other half of this problem — what version drift costs you even when you can see it. Losing the ability to see it at all, mid-migration, is strictly worse than having stale data: stale data is at least honest about being stale.
This connects to the broader API versioning practice too — see REST API versioning best practices for how version telemetry fits into a deprecation policy generally, independent of whether you're migrating off Stainless.
What to do this week
- Pull one real request from your current SDK's logs and confirm exactly which
X-Stainless-*headers it sends — don't assume the table above matches your SDK without checking, since this varies by generated client. - Add the equivalent header or User-Agent convention to your new SDK's client-construction code before you generate a version you intend to ship, not after.
- Update your dashboards and alerting queries to read both header sets, and put a calendar reminder on when to retire the old one.
- If you're mid-migration off Stainless generally, the four-step migration guide covers the rest of the cutover, including what stays the same (package names, auth env vars) and what doesn't.
FAQ
Do all Stainless-generated SDKs send the same X-Stainless-* headers?
The core set — X-Stainless-Lang, X-Stainless-Package-Version, X-Stainless-OS, X-Stainless-Arch, X-Stainless-Runtime, X-Stainless-Runtime-Version — follows a consistent pattern based on the openai-python implementation, but confirm against your own SDK's source rather than assuming every language and every generated client sends identical headers.
Will my new SDK send these headers automatically if I switch generators?
No. Whatever tool replaces Stainless generation — OpenAPI Generator, oapi-codegen, a hosted platform — starts from a clean client template with no telemetry headers unless you configure them. This is exactly the gap this post exists to flag before it costs you visibility.
Should I keep using the X-Stainless-* header names for my new SDK, for compatibility?
You can, and it minimizes changes to existing log queries. The tradeoff is that new headers named after a vendor you've left can be confusing to a future engineer reading your logs. A neutral, product-named header (X-YourSDK-Lang) is more common practice going forward, at the cost of a one-time query update.
How long should I dual-read both header sets?
At least one full release cycle, long enough for most active customers to upgrade past your last Stainless-generated SDK version. For APIs with slow-moving enterprise customers, a full quarter is safer than a month.
Does this affect SDKs I haven't touched yet, still on the old generator?
No — this only matters for SDKs generated by whatever replaces Stainless. Any SDK still built by your last Stainless generation run keeps sending its existing headers unchanged; the gap only opens once you generate and ship something new.
Start a free Sourced report against your OpenAPI spec, or create hosted docs from your repo — the compatibility report on every run is built to catch exactly this class of "the new package looks fine but something customer-facing quietly changed" issue before you publish.