Wrapping endpoints is the easy half of an SDK. It is mechanical, and a generator has always been able to do it.
The other half is what your customers file tickets about. Retries that back off properly instead of hammering a struggling server. A cache that does not hand back a response body somebody already read. Idempotency keys that stay the same across a retry, which is what they exist to do, and a detail that is easy to miss. Pagination that knows the difference between "there is a cursor" and "there is another page", so it stops instead of looping forever on the last one.
None of that is hard exactly. It is just fiddly, easy to get subtly wrong, and you get to write it again for every language you ship. Six languages means six retry policies, six chances to invert a condition, and six code reviews by people who each know one of those languages well.
So we generate it. Nineteen features, opt-in, configured from the constructor, implemented once per language and regenerated whenever your spec changes.
Add the ones you want. Each is off until you switch it on, and an unactivated feature is never constructed.
Resilience
retry backs off exponentially, adds jitter, and honours a Retry-After when the server sends one. timeout puts a deadline on a request and aborts the transport. ratelimit is a token bucket that keeps you inside the quota instead of discovering it through 429s. cache serves safe reads from a bounded TTL store.
Correct writes
idempotency stamps a key on every mutating call, generated before the request is built so it stays the same across every retry of that call. This is the feature that stops a retried order becoming three orders. Turn it on the same day you turn on retry.
Large result sets
paging reads whichever convention the API picked, a Link header, X-Next-Page, a body cursor, a GraphQL Relay connection, and hands you one normalised answer. streaming iterates a list result item by item, so you are not holding the whole array in memory.
Observability
telemetry opens a span per operation and propagates W3C traceparent. metrics counts calls and latency, in total and per operation. audit writes one structured record per call: actor, entity, operation, outcome, correlation id. debug keeps a ring buffer of recent calls with secrets already redacted. clienttrack and log fill in the rest.
Governance and spend
rbac checks the caller's permissions locally and fails fast, before the request leaves the process, so your UI never offers an action the user cannot perform. cost prices every call, attributes the spend to an operation and a caller, and can refuse a call once a budget is gone. proxy routes outbound calls through the corporate proxy, from options or the standard environment variables. secrets resolves the API credential through a provider chain rather than taking it as a literal, and exchanges a refresh token for short-lived access tokens when one expires.
Testing
test is an in-memory mock of your own API, seeded from data you supply, so the test suite needs no server. netsim injects latency, transient failures, 429s and total outages, deterministically. Together they let you prove your retry logic works without a network and without a wall clock.
That is all of it. Every feature takes an active flag and its own options, all with defaults that are reasonable on their own.
Eight of the features work by wrapping the transport, and each wraps whatever is already installed, so the order you list them in is the order they nest:
Which matters more than it looks. In that default chain one timeout deadline covers the whole retry sequence. If you want a deadline per attempt, list timeout first and it nests inside. The reference documents the default chain.
One default to override. Map order is alphabetical, so cache lands inside cost, exactly as the chain above shows. That means a response served from cache still passes through cost and gets charged for money nobody spent. Using both? List them in array form with cost first, so cache wraps it and a hit never reaches the meter:
The newest of the nineteen, built for metered APIs.
Metered APIs bill per call, and increasingly per token. Once an agent is driving your SDK, the question stops being "did that work" and becomes "what did that just spend, and who spent it".
cost prices each call from whichever source your API provides: a rate table you write, a cost header the server returns, a usage figure in the response body, or a flat per-call unit. It then attributes the spend two ways, to the operation and to the caller, so an invoice can be read as either.
Set a budget and, with onBudget: 'deny', the next operation is refused before an endpoint is resolved, so a runaway loop stops costing money at the point it stops being useful.
The budget is a cutoff, not a hard cap. The check asks whether you have already spent the budget, not whether this call will exceed it, because what a call costs is usually not known until after it has been made. A reported figure arrives with the response; a retried call is charged per attempt. So the last admitted call can carry you past the number: give it a budget of 2 and hand it one call that prices at 5, and you spend 5. It bounds a run, it does not guarantee a maximum. Size it a call's worth below the number you cannot exceed.
One detail to know: money is spent per HTTP attempt, but it is owed by an operation. A call that retried twice cost you three times, because your provider charged three times, and it should still show up as one call. So cost works at both seams, pricing at the transport and attributing at the end of the pipeline. That also means the order matters. Put cost inside cache, as the array above does, or a response served from cache is charged for money nobody spent.
Retries transient failures with exponential backoff and jitter. Honours Retry-After. Does not retry a 404 or a 422, because the server understood you and said no.
timeout
A deadline per request. Signals an AbortController, so a live fetch is cancelled rather than left running.
ratelimit
A token bucket at a rate and burst you choose. Requests wait for a token instead of being rejected.
cache
TTL cache for safe reads, bounded and keyed by method plus URL. Normalises one-shot response bodies, so a hit is readable more than once.
idempotency
An Idempotency-Key on mutating calls, set once per operation and stable across retries. Never overwrites a key you supplied.
paging
Writes page, limit or cursor on the way out. Reads Link, X-Next-Page, X-Total-Count, body cursors and GraphQL Relay pageInfo on the way back.
streaming
An async iterator over list results. Optional chunking, and an AbortSignal to stop early.
telemetry
A span per operation with W3C traceparent, X-Trace-Id and X-Span-Id headers. Export each finished span wherever you keep traces.
metrics
Counts and latency: total, and broken down by entity and operation. Failed calls counted once, not twice.
audit
One record per operation: sequence, timestamp, actor, entity, operation, outcome, status, correlation id. Push them to a SIEM as they happen.
debug
A bounded ring buffer of recent calls: method, URL, status, duration. Authorization, cookies and API keys are masked, so a trace is safe to paste into a ticket.
clienttrack
A stable per-client session id and a fresh id on every request, plus a descriptive User-Agent, so your server can correlate traffic.
rbac
Required permissions per entity and operation, checked before the endpoint is even resolved. Optional default-deny.
cost
Prices every HTTP attempt from a rate table, a response header, a body usage figure or a flat unit. Attributes the spend per operation and per caller, and enforces a budget.
proxy
Outbound proxy routing from options or HTTPS_PROXY. Bypass list included.
secrets
Resolves the API credential through a provider chain instead of a literal in the constructor. Optionally exchanges a refresh token for a short-lived access token, and retries the call that hit the 401.
log
Structured logging at every pipeline stage. Verbose by design, for diagnosis.
test
An in-memory mock of your API, seeded from your own data. Serves reads and writes, and understands your response envelopes.
netsim
Latency, first-N failures, every-Nth failures, seeded random failures, 429s with Retry-After, and hard outages.
Every feature on this page is implemented for every language target the generator supports, with the same option names, the same defaults, and the same inspectable state. Your Go customers get the same retry policy your TypeScript customers get.
Each target ships a feature test suite that runs offline, and the generator's own suite drives every feature's template source through a simulated pipeline. A target that diverges fails the build.
No server, no network, no wall clock, and an exact assertion on the backoff. Every feature that measures time or generates an id takes an injectable version of it for exactly this reason.
Are these features available in every language, or just TypeScript?
Every one of them, in every language target we generate. The behaviour, the option names and the inspectable state are the same across languages; only syntax and idiom differ. That is the reason to generate them rather than hand-write them: a hand-written retry policy is one more chance to get the backoff wrong in every language you ship.
Do I have to take all nineteen?
No. You add the features you want, and only those get copied into your project. Each one is then off until you activate it in the constructor. A project that wants retries and nothing else carries retries and nothing else.
What happens when I combine retry and timeout?
They compose, and the order decides what you get. Seven features work by wrapping the transport, and each wraps whatever is already installed. By default timeout ends up outside retry, so one deadline covers the whole retry sequence. Pass the features as an ordered array instead of a map and you can put timeout inside retry for a per-attempt deadline. The feature reference lists the default chain.
Can I test retry and timeout behaviour without a network?
Yes. The test feature mocks your API in memory, netsim injects the failures on a counter, and every feature that measures time or generates an id takes an injectable clock or generator. So a test asserts on exact backoff delays rather than sleeping and hoping.
Can I write my own feature?
Yes. A feature is a model file plus per-language source that implements the pipeline hooks it declares, or wraps the transport. You can keep it in your own project, or publish it as an sdkgen package for other people to install. The nineteen shipped features are written against the same interface yours would use.
What is the runtime cost of all this?
A feature you have not activated is not constructed, so it costs nothing. An active one costs what it does: a token bucket does arithmetic, a cache holds a bounded map, telemetry allocates a span per call. Nothing here phones home, and nothing polls.