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
AsyncNozle: the Nozle client for asyncio apps (FastAPI and the like). Same options (api_key or NOZLE_SECRET_KEY, host, base_url, events_url, timeout, max_retries, retry_delay), namespaces and methods, awaited; async with AsyncNozle() as nozle: or await nozle.aclose(). Requests go through httpx.AsyncClient (pip install 'nozle-sdk[httpx]') and retries wait with asyncio.sleep. Each call is written once and runs on both clients, so validation, errors, retries, idempotency keys, timestamps and left-out unset fields are the same. webhooks.verify is awaited on AsyncNozle (a jwt endpoint reads the public key). capture_async_http_client and wrap_http_client on an httpx.AsyncClient record usage on the event loop with AsyncNozle. Nozle is unchanged.
nozle.alerts.list / get / create / update / delete manage subscription usage alerts (subscription_id=) and wallet balance alerts (customer_id=, wallet_code=). create(alert_type, code, thresholds, *, name=None, feature_code=None, ...); feature_code is required for the feature_* types. Typed as Alert and AlertPage.
nozle.invoices.list(customer_id, *, status=None, payment_status=None, payment_overdue=None, issuing_date_from=None, issuing_date_to=None, page=None, per_page=None) (newest first; payment_overdue=True keeps only unpaid invoices past their due date), get(invoice_id) and download_url(invoice_id) (the PDF URL, or None while it is generated). Typed as Invoice and InvoicePage.
current_usage(customer_id, subscription_id=None) and past_usage(customer_id, subscription_id=None, *, periods=None) (newest first, a list) read Core usage; without a subscription ID the customer's one active subscription is used. Typed as CustomerUsage.
nozle.webhooks.verify(body, headers, *, hmac_key=None) checks X-Nozle-Signature (jwt: Nozle's public key, read once with the secret key and cached; hmac: the signing secret) and returns a NozleWebhookEvent, or raises WebhookVerificationError with a code. No new dependency.
api_key is optional. When it is not passed, Nozle() reads the NOZLE_SECRET_KEY environment variable and raises ValueError if that is not set either. An explicit api_key is used as before.
host keyword argument (default https://api.nozle.app). It fills in base_url ({host}/engine) and events_url ({host}/core) when they are not given. base_url and events_url keep their meaning and win over host.
Opt-in retries: Nozle(max_retries=2, retry_delay=0.5) (default max_retries=0, no retries). Network errors, timeouts, 429 and 5xx are retried with backoff, or after the Retry-After seconds when sent, 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 transition preview. subscribe(), checkout() and other writes without a key are never retried. The timeout applies to each attempt.
NozleAPIError.request_id (the X-Request-Id response header, which Core sends and the Engine does not yet) and NozleAPIError.retry_after (seconds, from Retry-After, as a number or an HTTP date). Both are None when the service did not send them. The message is unchanged.
entity_subscriptions.change_plan accepts billing_time, which the API already reads.
Usage capture reports once per client and event code when no Feature has that code: on_error (or a NozleTrackingWarning) gets an error with code == "feature_not_found", because the usage is stored but not billed. Recording is not blocked.
Usage capture for any LLM client built on httpx or httpx2. nozle.wrap_http_client(client, customer_id=...) makes the client your SDK takes as http_client (for example openai.DefaultHttpxClient() or anthropic.DefaultHttpxClient()) record the token usage of every successful LLM response; nozle.capture_http_client(...) and capture_async_http_client(...) build a new client the same way. Usage is recognised by the response shape: OpenAI-compatible usage (any OpenAI-compatible API, including the Responses API), Anthropic usage and Gemini usageMetadata, for JSON responses and streams. Chat completion streams are asked for usage (stream_options.include_usage; include_stream_usage=False turns it off). The customer comes from customer_id (a string or a function per request) or the X-Nozle-Customer-Id request header, which is not sent to the provider. X-Nozle-Feature sets the feature property and is also removed. Recording runs in a background thread (with AsyncNozle, a task on the event loop) and never changes or fails the provider response; failures go to on_error or a NozleTrackingWarning. Install with pip install 'nozle-sdk[httpx]'.
nozle.record_usage(customer_id, model=..., input_tokens=..., output_tokens=...) records usage your code read itself, and raises if it could not be recorded.
Changed
Usage events recorded by wrap_openai, wrap_anthropic and the new capture use the provider's response id as the transaction id (llm-<format>-<id>) when there is one, so the same response is never recorded twice. Events also carry provider_host when known.
Removed
PingResult.version and SubscriptionOptions.blocked_reason are removed from the types. The API has never sent either field, so code reading them always got nothing.
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.
Requests leave out every optional field you did not set instead of sending JSON null, so the API applies its defaults. This is done once in the transport for every call; it fixes entity_subscriptions.checkout, change_plan, checkout_many and cancel, the subscription transition target_plan_code, and an Entity upsert without a name (leaving the name out means the same as null).