The hard parts, generated in

Most generators give you typed wrappers around your endpoints. Voxgig also gives you the eighteen things you would otherwise write by hand, in every language, forever.

SDK Generator overview Read the reference

The part nobody budgets for#

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 actually 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 the entire point of them and the detail most implementations 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. Eighteen features, opt-in, configured from the constructor, implemented once per language and regenerated whenever your spec changes.

Eighteen features, six jobs#

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 actually 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.

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.

What using them looks like#

Two steps, at two different times. Add the feature to your project once, then activate it per client.

# once, in your project
voxgig-sdkgen feature add retry,timeout,idempotency
// wherever you construct a client
const client = new MyapiSDK({
  apikey: process.env.MYAPI_APIKEY,
  feature: {
    retry:       { active: true, retries: 4, maxDelay: 5000 },
    timeout:     { active: true, ms: 10000 },
    idempotency: { active: true },
  },
})

That is the whole API. Every feature takes an active flag and its own options, all with defaults that are reasonable on their own.

Seven 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:

call → timeout → retry → ratelimit → proxy → netsim → cost → cache → HTTP

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. We document the default rather than leaving you to find it in production.

One place the default is wrong, and you should override it. 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:

feature: [
  { name: 'cost',  active: true, unit: 0.002 },   // inner: real calls only
  { name: 'cache', active: true, ttl: 30000 },
]

// call → … → cache → cost → HTTP

Knowing what a call costs#

The newest feature, and the one that changed shape while we built it.

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 actually gives you: 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.

const client = new MyapiSDK({
  feature: [
    { name: 'cost', active: true,
      path: 'usage.total_tokens', perUnit: 0.00001,
      budget: 5.00, onBudget: 'deny' },
    { name: 'retry', active: true, retries: 3 },
  ],
})

await client.Completion().create({ prompt }, { actor: 'agent:planner' })

client._cost.total     // { calls: 1, attempts: 1, amount: 0.0134, ... }
client._cost.actors    // { 'agent:planner': { calls: 1, amount: 0.0134 } }

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.

Be clear about what that is, though: 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 actually cannot exceed.

The detail worth knowing: 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.

All eighteen#

FeatureWhat it does for you
retryRetries 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.
timeoutA deadline per request. Signals an AbortController, so a live fetch is cancelled rather than left running.
ratelimitA token bucket at a rate and burst you choose. Requests wait for a token instead of being rejected.
cacheTTL cache for safe reads, bounded and keyed by method plus URL. Normalises one-shot response bodies, so a hit is readable more than once.
idempotencyAn Idempotency-Key on mutating calls, set once per operation and stable across retries. Never overwrites a key you supplied.
pagingWrites 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.
streamingAn async iterator over list results. Optional chunking, and an AbortSignal to stop early.
telemetryA span per operation with W3C traceparent, X-Trace-Id and X-Span-Id headers. Export each finished span wherever you keep traces.
metricsCounts and latency: total, and broken down by entity and operation. Failed calls counted once, not twice.
auditOne record per operation: sequence, timestamp, actor, entity, operation, outcome, status, correlation id. Push them to a SIEM as they happen.
debugA 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.
clienttrackA stable per-client session id and a fresh id on every request, plus a real User-Agent, so your server can correlate traffic.
rbacRequired permissions per entity and operation, checked before the endpoint is even resolved. Optional default-deny.
costPrices 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.
proxyOutbound proxy routing from options or HTTPS_PROXY. Bypass list included.
logStructured logging at every pipeline stage. Verbose on purpose, for when you are actually looking.
testAn in-memory mock of your API, seeded from your own data. Serves reads and writes, and understands your response envelopes.
netsimLatency, first-N failures, every-Nth failures, seeded random failures, 429s with Retry-After, and hard outages.

Every option and default, in the reference

The same in every language#

This is the claim worth checking before you believe any of the above.

A lot of tooling ships a rich TypeScript client and a thin wrapper everywhere else. Ours does not. 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.

We keep it that way with tests rather than intentions. Each target ships a feature test suite that runs offline, and the generator's own suite drives the real template source for every feature through a simulated pipeline. Parity is a build failure, not a promise.

Prove it works, offline#

Resilience code that has never failed is not tested code. So two of the eighteen exist to make failure cheap to reproduce.

const client = new MyapiSDK({
  feature: [
    { name: 'test',   active: true, entity: seedData },
    { name: 'netsim', active: true, failTimes: 2, failStatus: 503 },
    { name: 'retry',  active: true, retries: 3, minDelay: 10,
                      jitter: false, sleep: (ms) => sleeps.push(ms) },
  ],
})

await client.Product().load({ id: 'p1' })
// sleeps === [10, 20], client._retry.attempts === 2

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.

Honest about what this is#

What these features are

  • Generated source in your repo, under your license. There is no runtime to install and no Voxgig service in the call path.
  • Off until you switch them on. An unactivated feature is never even constructed.
  • The same behaviour, option names and recorded state in every language we generate.
  • Covered by a generated test suite that runs offline.

What they are not

  • rbac is not a security boundary. It gives fast local failures and keeps a UI honest. Your server still has to authorize.
  • cache is a plain TTL cache. It does not read Cache-Control or revalidate with ETag.
  • ratelimit is per client instance. Two processes get two buckets.
  • log is a diagnosis tool, not a production logger. Use telemetry, metrics or audit in production.

FAQ#

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 whole reason to generate them rather than hand-write them: a hand-written retry policy in six languages is six chances to get the backoff wrong.

Do I have to take all eighteen?

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 generator documents the default chain so you are not guessing.

Can I test retry and timeout behaviour without a network?

Yes, and this is the part we are most pleased with. 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 eighteen 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.

18 features
from retry to cost, all opt-in
Every language
the same behaviour, not a TypeScript-only tier
Offline tests
mock transport plus injectable clocks
MIT
generated into your repo, yours to change

Generate an SDK that already knows how to retry#

Two minutes from a clean checkout. Add the features you want, activate the ones you need.

Browse the SDK CatalogTalk to Voxgig

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.