Sign In

@budgetary/sdk

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@budgetary/sdk

TypeScript SDK for the Budgetary API.

latest
Source
npmnpm
Version
0.9.0
Version published
Weekly downloads
63
-89.36%
Maintainers
1
Weekly downloads
 
Created
Source

@budgetary/sdk

TypeScript SDK for the Budgetary API — a hosted service that returns probabilistic pre-inference estimates of token spend for LLM queries. The SDK is a thin, typed HTTP wrapper with zero runtime dependencies; it speaks the v1 contract documented at docs/api-contract.md.

Install

npm install @budgetary/sdk
# or: pnpm add @budgetary/sdk   /   yarn add @budgetary/sdk

The package publishes both ESM and CommonJS builds with type declarations for each, so import and require both work from TypeScript and JavaScript.

Quickstart

import { BudgetaryClient, normalizeScenario } from "@budgetary/sdk";

// A free `bg_test_` key works immediately for development; `bg_live_` is the
// production key (and must be on an active plan). Get one at https://budgetary.tools
const client = new BudgetaryClient({ apiKey: process.env.BUDGETARY_API_KEY! });

const estimate = await client.estimate("fix the flaky test in the payments service", {
  model: "claude-opus-4-7",
  context: { host: "sdk", projectId: "proj_kx7" },
});

if (estimate.void || estimate.distribution === null) {
  // The server declined to estimate (out of domain). Not an error.
  console.log("No confident estimate for this query.");
} else {
  const { p10, p50, p90 } = estimate.distribution;
  console.log(
    `~${p50} tokens (p10–p90: ${p10}${p90}), scenario: ${normalizeScenario(estimate.scenario)}`,
  );
}

Reading an estimate

A successful estimate is a range, not a point. distribution gives p10, p50, and p90 combined input+output tokens; treat p50 as the midpoint of that range, not a guaranteed cost. Three fields tell you how much to trust it:

  • scenario (contract §5) — confident (the range is reliable), uncertain (supported but wide), sparse_evidence (near the edge of what's been seen), or out_of_domain (returned with void: true). The server may add labels at any time, so fold unknown values with normalizeScenario(scenario), which maps anything unrecognized to "uncertain" — never treat an unknown label as confident.
  • confidence — a single [0, 1] quality summary. Lower means a wider range and a rougher midpoint. Read it alongside scenario, not as a probability.
  • voidtrue when the server declined to estimate (out of domain). Branch on it before reading distribution, which is null in that case. A void response is not an error.

Don't render a low-confidence or void estimate as if it were a precise number — surface the range and the caveat.

The idiomatic read narrows distribution by branching on void first, then destructures in the non-null branch:

if (estimate.void || estimate.distribution === null) {
  // no prediction — render the caveat
} else {
  const { p10, p50, p90 } = estimate.distribution; // non-null here
}

Closing the loop

After a run, submit the realized counts so future estimates calibrate, then read them back from the ledger. Token counts must be measured, never guessed.

// Submit realized usage for a prior estimate (idempotent on estimateId).
await client.submitActuals({
  estimateId: estimate.estimateId,
  tokensIn: 12_340,
  tokensOut: 36_210,
  success: true,
  durationMs: 420_000,
});

// Read the predicted-vs-actual ledger.
const page = await client.getLedger({ projectId: "proj_kx7", limit: 50 });
for (const entry of page.entries) {
  console.log(entry.queryExcerpt, entry.predicted.p50, entry.actual?.total ?? "pending");
}

submitActuals returns 202 and is idempotent on estimateId (safe to retry on a network failure). getLedger is paginated — pass the returned nextCursor as after for the next page, and set includeOrphans: true to include estimates with no actuals yet.

Error handling

Every documented HTTP status maps to a typed exception. Retryable errors (429, 5xx) are retried automatically with exponential backoff and jitter — by the time an error reaches your code, retries have already been exhausted.

import {
  BudgetaryClient,
  BudgetaryRateLimitError,
  BudgetaryValidationError,
} from "@budgetary/sdk";

try {
  await client.estimate(query);
} catch (err) {
  if (err instanceof BudgetaryRateLimitError) {
    console.warn(`rate limited; retry after ${err.retryAfterSeconds}s`);
  } else if (err instanceof BudgetaryValidationError) {
    console.error(`bad input: ${err.message}`);
  } else {
    throw err;
  }
}

The full hierarchy:

  • BudgetaryError — base class (code, httpStatus, requestId).
  • BudgetaryAuthError401 (key missing, invalid, or revoked — re-authenticate).
  • BudgetaryPermissionError403 (valid key, but lacks scope or an active plan — distinct from 401).
  • BudgetaryNotFoundError404.
  • BudgetaryValidationError400, 409, 413.
  • BudgetaryRateLimitError429; exposes retryAfterSeconds.
  • BudgetaryServerError5xx.
  • BudgetaryNetworkError — no response received (timeout, network, abort).

Constructor options

OptionTypeDefaultNotes
apiKeystringrequiredSent as Authorization: Bearer <apiKey>.
baseUrlstringhttps://api.budgetary.toolsOverride for staging or self-hosted endpoints.
timeoutMsnumber10_000Per-request timeout via AbortSignal.timeout.
maxRetriesnumber4Maximum retries on 429 and 5xx. Total attempts = maxRetries + 1 (5, per contract §8).
fetchImpltypeof fetchglobal fetchInject a mock fetch in tests.

Idempotency

estimate() accepts an optional clientRequestId that is forwarded to the server as client_request_id. It is a correlation id only: the server accepts it but currently ignores it — it does not provide idempotent replay. A retry with the same client_request_id produces a new estimate_id, recomputes, and is billed as a separate estimate. Do not rely on it for dedup.

POST /v1/actuals is idempotent on estimate_id, and is safe to retry on network failure. See §8 of the API contract.

// default — SDK generates a UUID
await client.estimate("...");

// caller-supplied
await client.estimate("...", { clientRequestId: "my-deterministic-id" });

// explicit opt-out — no client_request_id sent
await client.estimate("...", { clientRequestId: null });

Naming conventions

  • Wire protocol uses snake_case (estimate_id, tokens_in, client_request_id).
  • SDK surface uses camelCase (estimateId, tokensIn, clientRequestId).

The SDK converts at the HTTP boundary in both directions; callers never see snake_case.

Reference

For the full API contract — endpoints, error codes, scenario labels, idempotency semantics — see docs/api-contract.md.

Licensed under Apache-2.0.

FAQs

Package last updated on 09 Aug 2026

Related posts