Skip to content

2.0.0-beta.55 - AI Gateway Spend Caps

An AI Gateway sits in front of your model calls, which means it’s also where a runaway loop turns into a runaway bill. beta.55 puts the guardrail in your stack: Cloudflare.AiGatewaySpendingLimit sets a hard dollar cap on cumulative AI Gateway spend across every gateway in the account, declared and reconciled like any other resource.

Amounts are in cents (Cloudflare’s native unit — minimum 1_00 = $1.00), accumulated over a daily, weekly, or monthly window with a fixed (reset on the boundary) or sliding (rolling window) strategy:

import * as Cloudflare from "alchemy/Cloudflare";
// Cloudflare requires one manual credit top-up before a spending limit
// can be set. `topUp` reconciles that requirement: the provider observes
// the billing state and only charges the account's default payment
// method if the account has never topped up.
const cap = yield* Cloudflare.AiGatewaySpendingLimit("ai-spend-cap", {
amount: 250_00, // cents -> $250.00 (minimum 1_00 = $1.00)
duration: "monthly",
topUp: { amount: 10_00 }, // cents -> $10.00 (Cloudflare minimum)
});

The topUp prop deals with Cloudflare’s bootstrap requirement: an account can’t set a spending limit until it has loaded Unified Billing credits via at least one manual top-up (NO_MANUAL_TOPUP). The provider observes first_topup_success on the live billing API and only makes the one-time charge when the account has never topped up — once bootstrapped, the prop is inert and never charges again. If the payment needs interactive confirmation (3-D Secure), the resource fails with a typed AiGatewaySpendingLimitTopupRequired error and the top-up must be completed in the dashboard.

Auto recharge is on by default — when the credit balance drops below threshold, Cloudflare recharges by amount automatically:

const cap = yield* Cloudflare.AiGatewaySpendingLimit("ai-spend-cap", {
amount: 250_00,
duration: "monthly",
topUp: { amount: 20_00, threshold: 10_00 }, // recharge $20 below $10
});

Set topUp: { amount: 10_00, autoRecharge: false } for a one-time bootstrap only.

One caveat: Cloudflare stores a single limit per account, so this resource is a per-account singleton — declare exactly one. Thanks Matthew Aylward (#569).

Fetcher utils on alchemy/Cloudflare/Bridge

Section titled “Fetcher utils on alchemy/Cloudflare/Bridge”

The fetcher/socket interop helpers — fromCloudflareFetcher, toCloudflareFetcher, toHttpClient, fromCloudflareSocket — are now exported from the alchemy/Cloudflare/Bridge entrypoint, and fromCloudflareFetcher / fromCloudflareSocket accept the global Fetcher / Socket types directly (#581).

That combination matters inside a framework worker (TanStack Start, Astro, …), where your Effect backend arrives as a plain service binding on env. Two calls turn it into a fully typed Effect HttpClient — no casts:

import * as Cloudflare from "alchemy/Cloudflare/Bridge";
const client = Cloudflare.toHttpClient(
Cloudflare.fromCloudflareFetcher(env.BACKEND),
);
const res = await client
.get(`https://backend/?key=${encodeURIComponent(key)}`)
.pipe(Effect.runPromise);
return HttpServerResponse.toWeb(
HttpServerResponse.fromClientResponse(res),
);

The request rides the in-account service binding, not the public network. The cloudflare-tanstack example’s /api/hello route now serves the same object four ways side by side — the raw R2 binding, the service binding’s fetch, typed RPC (toRpcAsync), and this HttpClient form — each covered by an integration test.

  • Fixed a dangling process that kept alchemy deploy / alchemy destroy from exiting — local RpcProvider service layers (providerServices / providerServicesEffect) now only construct when AlchemyContext.dev is set, so one-shot CLI runs no longer spin up the local dev provider machinery (#580)