You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
nozle.captureFetch(options, fetch?) returns a fetch that records the token usage of every successful LLM response. Pass it as the fetch option of the OpenAI or Anthropic SDK, a Vercel AI SDK provider, LangChain, or any client that accepts one. Usage is recognised by the response shape: OpenAI-compatible usage (any OpenAI-compatible API, including the Responses API), Anthropic usage and Gemini usageMetadata. Streams are read as the app reads them; chat completion streams are asked for usage (stream_options.include_usage, turn off with includeStreamUsage: false). When the app aborts a stream, the usage already seen is still recorded. The customer comes from customerId (a string or a function per request) or the X-Nozle-Customer-Id request header, which is not sent to the provider. Recording never changes or fails the provider response; failures go to onError. detectUsage and normalizeGeminiUsage and the DetectedUsage and ProviderUsageFormat types are exported for your own parsing.
nozle.recordUsage({ customerId, model, inputTokens, outputTokens, ... }) records usage your code read itself, and rejects if it could not be recorded.
captureFetch reports once per client and event code when no Feature has that code (onError gets an error with code: "feature_not_found"): the usage is stored but not billed until a metered Feature with that code exists. Recording is not blocked. wrapOpenAI and wrapAnthropic take the same onError; without it their failures stay silent as before.
createBillingHandler(nozle, { getCustomerId }) returns a web-standard (request) => Response handler that serves the @nozle-js/react billing hooks with your secret key. The last path segment picks the route: subscription (with past_due, true when the customer has an unpaid invoice past its due date), entitlement, credits, credit-history, usage, preview, invoices (the customer's finalized invoices) and invoice-download (404 for another customer's invoice). BILLING_ROUTES lists them. The customer always comes from getCustomerId, never from the request. Works in Next.js route handlers, Hono, Remix, Bun and Deno.
getSubscriptions(customerId), currentUsage(customerId, { subscriptionId? }) and pastUsage(customerId, { subscriptionId?, periods? }) read Core subscriptions and usage; their types (CoreSubscription, CustomerUsage) come from the OpenAPI spec.
nozle.invoices.list(customerId, options), get(invoiceId) and downloadUrl(invoiceId) (the PDF URL, or null while it is generated). list takes paymentOverdue to return only invoices past their due date.
nozle.alerts.list / get / create / update / delete manage usage alerts of a subscription ({ subscriptionId }) and balance alerts of a wallet ({ customerId, walletCode }). Nozle sends alert.triggered when one fires.
nozle.webhooks.verify({ body, headers, hmacKey? }) checks X-Nozle-Signature (jwt: Nozle's public key, read once with the secret key and cached; hmac: the signing secret) and returns the event (id, type, objectType, organizationId, object, payload), or throws WebhookVerificationError with a code. createWebhookHandler(nozle, { hmacKey?, onEvent }) returns a Fetch API handler that answers 200, 400 for a forged request, or 500 when onEvent throws.
apiKey is optional. When it is not passed, new Nozle() reads the NOZLE_SECRET_KEY environment variable and throws if that is not set either. An explicit apiKey is used as before.
host option (default https://api.nozle.app). It fills in baseUrl (${host}/engine) and eventsUrl (${host}/core) when they are not given. baseUrl and eventsUrl keep their meaning and win over host.
Opt-in retries. Set maxRetries (default 0, no retries) and optionally retryDelayMs (default 500, doubling up to 10 seconds, or the Retry-After header when sent) on new Nozle({...}). Network errors, timeouts, 429 and 5xx are retried, only for requests that are safe to repeat: reads, calls that carry an Idempotency-Key (such as usage.track), event, cost event and customer writes, and the read-only usage.check and subscription transition preview. subscribe(), checkout(), checkAndDeduct() and other writes without a key are never retried. The timeout option now applies to each attempt.
NozleApiError adds requestId (the X-Request-Id response header, which Core sends and the Engine does not yet) and retryAfter (seconds, from Retry-After).
Changed
Every call that gets a non-2xx response now throws NozleApiError (status, code, errorDetails, hasCode()), not only checkout and margin calls. This covers track, usage.track, usage.check, can, costEvents.track, subscribe, cancelSubscription, ping, plans, checkAndDeduct, the subscription lookup, customers, creditSystems, credits, entities and entitySubscriptions. The message keeps its <operation> failed: <status> <statusText> form, and the class still extends Error, so existing catch code keeps working; the subscription lookup message now carries the status text too.
Usage events recorded by wrapOpenAI, wrapAnthropic and captureFetch use the provider's response id as the transaction id (llm-<format>-<id>), so the same response is never recorded twice. Events also carry provider_host when known.
CanResult.limit and CanResult.remaining are typed number | null. The API has always sent null for a feature without a limit, while the type said number | undefined. Code that checked === undefined for "no limit" should check == null.
Deprecated
checkAndDeduct() is marked @deprecated; use usage.check() and usage.track() with a credit system.
Removed
PingResult.version and SubscriptionOptions.blocked_reason are removed from the types. The API has never sent either field.
Fixed
usage.track() reuses the timestamp it filled in when the same idempotency key is sent again on the same client, so a retry no longer fails with 409 idempotency_key_reused. The client remembers the last 1000 keys; a retry from another process or after a restart must pass timestamp.
wrapOpenAI and wrapAnthropic return the SDK's own APIPromise and Stream, so withResponse(), asResponse(), chat.completions.parse(), messages.stream() / finalMessage(), stream.controller and toReadableStream() work on a wrapped client. Usage is recorded once, when the response is read (not for asResponse(), which leaves the body to you); a stream records when it is fully iterated. Errors reject only to the caller.
The optional peer dependency is @anthropic-ai/sdk, the package wrapAnthropic wraps, instead of the unrelated anthropic package.