zuplo 7.12.1 → 7.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ ---
2
+ title: "AI Gateway Budget Alerts"
3
+ sidebar_label: "Budget Alerts"
4
+ description:
5
+ Get notified in the portal and by email when an AI Gateway pool, app, or
6
+ person crosses a budget rule's warning or block amount, and mute alerts you
7
+ don't need.
8
+ ---
9
+
10
+ Budget alerts tell you when usage crosses an amount set in a budget rule, so you
11
+ don't have to watch the analytics to find out. There's nothing to set up. Each
12
+ **Warn** or **Block** row in a [budget rule](./usage-limits.mdx) sets an amount;
13
+ crossing that amount sends an alert to the people who can see the pool or app.
14
+
15
+ ## What you're notified about
16
+
17
+ A budget rule caps cost, tokens, or requests over a UTC period and can have a
18
+ **Warn** row, a **Block** row, or both for the same meter and period. See
19
+ [Usage Limits](./usage-limits.mdx) for how to write one.
20
+
21
+ Each budget is reported once per row per period: the first time its usage
22
+ crosses the amount, and again after the period resets. A **By metadata** rule
23
+ gives every value its own budget, so each value is reported separately. Usage is
24
+ checked about once a minute, and an alert usually arrives within a minute or two
25
+ of the crossing. Alerts for several values of one rule are held for up to five
26
+ minutes so they arrive together.
27
+
28
+ | Alert | When it's sent | What it means |
29
+ | ----------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | **Warning** | Usage first crosses the **Warn** amount | Requests continue. Use the time left in the period to raise the limit or slow the caller down. |
31
+ | **Limit reached** | Usage first crosses the **Block** amount | Requests are rejected with `429 Too Many Requests`, or served by the app's [quota fallback](./fallback.mdx), until the period resets. The alert includes the reset time. See [When a limit is exceeded](./usage-limits.mdx#when-a-limit-is-exceeded). |
32
+
33
+ If one request pushes usage past both amounts, you get only a **Limit reached**
34
+ alert.
35
+
36
+ What the alert names depends on the rule's scope:
37
+
38
+ - A **Shared** rule names the gateway, pool, or app whose budget it is.
39
+ - A **By metadata** rule names the metadata value that crossed, such as one
40
+ `x-user` value. When several values of the same rule cross an amount within
41
+ five minutes, they arrive as one alert listing the values.
42
+ - The [User App](./user-apps.mdx)'s per-person budget names the person.
43
+
44
+ ## Who is notified
45
+
46
+ Alerts go to the people who can already see the usage in the Zuplo Portal:
47
+
48
+ - **A pool or an app**: everyone with view access to it. A pool member receives
49
+ alerts for that pool and the apps in it; someone with access to the whole
50
+ project receives alerts for every pool.
51
+ - **The User App**: only the Admins who can edit it, the people who set its
52
+ budget. Everyone else on the project can see only their own usage, so they
53
+ aren't told about the app's total or about other people.
54
+ - **A personal budget**: the person it belongs to, and the User App's Admins.
55
+
56
+ ## Where alerts arrive
57
+
58
+ Each alert is delivered two ways, and both are on by default:
59
+
60
+ - **In-app feed**: the notification bell in the Zuplo Portal header. The feed
61
+ links to the app, pool, or gateway page that shows the usage.
62
+ - **Email**: a message to the address on your Zuplo profile.
63
+
64
+ ## Turn alerts off
65
+
66
+ Turning alerts off changes only what you receive. Everyone else keeps their own
67
+ settings.
68
+
69
+ ### Turn off all budget alerts
70
+
71
+ To stop receiving budget alerts for every gateway, open your
72
+ [Profile](https://portal.zuplo.com/user/profile) in the Zuplo Portal and find
73
+ the **Notifications** section. On the **AI Gateway Budget Alerts** row, clear
74
+ **In-app Feed**, **Email**, or both.
75
+
76
+ ### Mute a gateway, pool, or app
77
+
78
+ To keep alerts for most of a gateway and silence the parts you don't need, mute
79
+ them from the analytics:
80
+
81
+ <Stepper>
82
+
83
+ 1. Open your AI Gateway project in the Zuplo Portal and go to
84
+ [**Observability → Analytics**](https://portal.zuplo.com/+/account/project/ai/analytics),
85
+ then select **Pools & Apps**.
86
+
87
+ 1. Select the alert bell on a pool card or an app row. It's the mute control for
88
+ that pool or app.
89
+
90
+ 1. Switch off **Warnings**, **Limit reached**, or both.
91
+
92
+ </Stepper>
93
+
94
+ Muting a pool mutes every pool and app inside it, including ones created later.
95
+ Under a muted pool, each pool and app shows locked switches and names the muted
96
+ pool. To receive alerts for any of them again, turn that pool's alerts back on.
97
+
98
+ The gateway card at the top of the **Pools & Apps** page covers the whole
99
+ gateway. Muting it mutes every pool, every app, the User App, and every person's
100
+ personal budget, your own included.
101
+
102
+ :::note{title="Alerts don't change enforcement"}
103
+
104
+ Muting an alert, or turning the category off on your profile, changes who is
105
+ told, not what happens. A **Block** row still rejects requests at its amount, or
106
+ sends them to the app's [quota fallback](./fallback.mdx), and a **Warn** row
107
+ still lets them through.
108
+
109
+ :::
110
+
111
+ ## Related resources
112
+
113
+ - [Usage Limits](./usage-limits.mdx) - Set the amounts that trigger these
114
+ alerts.
115
+ - [User Apps](./user-apps.mdx) - Set the per-person budget behind personal
116
+ budget alerts.
117
+ - [Fallback Models](./fallback.mdx) - Serve a cheaper model instead of a `429`
118
+ when a **Block** amount is reached.
@@ -16,8 +16,9 @@ active completions model.
16
16
  Two public primitives from `@zuplo/runtime` do the work:
17
17
 
18
18
  - `AIGatewayModels.load(context)` returns the cached provider catalog, including
19
- each model's capability, status, and per-token pricing. Credentials are never
20
- returned.
19
+ each model's capability, status, and token rate card (`tokenRateCard`): the
20
+ prices the gateway charges, which are the list prices with your project's
21
+ prices applied. Credentials are never returned.
21
22
  - `AIGatewayModelRouting.set(context, routing)` validates the routing, resolves
22
23
  provider credentials internally, and stores the selection the AI Gateway
23
24
  handler uses. `AIGatewayModelRouting.get(context)` reads the current
@@ -27,6 +28,7 @@ Two public primitives from `@zuplo/runtime` do the work:
27
28
 
28
29
  ```ts title="modules/cheapest-model.ts"
29
30
  import {
31
+ AIGatewayModelInfo,
30
32
  AIGatewayModelRouting,
31
33
  AIGatewayModels,
32
34
  ZuploContext,
@@ -37,6 +39,25 @@ interface CheapestModelOptions {
37
39
  providers?: string[];
38
40
  }
39
41
 
42
+ // The model's base input plus output price per token: the rates with no
43
+ // input-size tier or cache lifetime. Undefined when the model has no rate card
44
+ // or lacks either base rate.
45
+ function basePrice(model: AIGatewayModelInfo): number | undefined {
46
+ const baseRate = (category: "input" | "output") =>
47
+ model.tokenRateCard?.rates.find(
48
+ (rate) =>
49
+ rate.category === category &&
50
+ rate.minInputTokens === undefined &&
51
+ rate.maxInputTokens === undefined &&
52
+ rate.cacheTtl === undefined,
53
+ )?.costPerToken;
54
+ const input = baseRate("input");
55
+ const output = baseRate("output");
56
+ return input === undefined || output === undefined
57
+ ? undefined
58
+ : input + output;
59
+ }
60
+
40
61
  export default async function cheapestModel(
41
62
  request: ZuploRequest,
42
63
  context: ZuploContext,
@@ -49,18 +70,17 @@ export default async function cheapestModel(
49
70
  provider.models.map((model) => ({
50
71
  providerName: provider.providerName,
51
72
  model,
73
+ price: basePrice(model),
52
74
  })),
53
75
  )
54
76
  .filter(
55
- ({ model }) =>
56
- model.capability === "completions" && model.status === "active",
77
+ (candidate): candidate is typeof candidate & { price: number } =>
78
+ candidate.model.capability === "completions" &&
79
+ candidate.model.status === "active" &&
80
+ // Skip unpriced models instead of treating them as free.
81
+ candidate.price !== undefined,
57
82
  )
58
- .sort(
59
- (left, right) =>
60
- left.model.inputCostPerToken +
61
- left.model.outputCostPerToken -
62
- (right.model.inputCostPerToken + right.model.outputCostPerToken),
63
- );
83
+ .sort((left, right) => left.price - right.price);
64
84
 
65
85
  const cheapest = candidates[0];
66
86
  if (!cheapest) {
@@ -90,7 +90,9 @@ the app's **Policies** tab and add budget rules capping spending, tokens, or
90
90
  requests over an hourly, daily, weekly, or monthly period—either for the app as
91
91
  a whole or per metadata value, so each caller gets its own budget. Pool and
92
92
  gateway limits apply to every app automatically, even when the app's chain
93
- doesn't include Budgets and Costs. See [Usage Limits](./usage-limits.mdx).
93
+ doesn't include Budgets and Costs. See [Usage Limits](./usage-limits.mdx). The
94
+ people who can view the app are notified when it crosses a **Warn** or **Block**
95
+ amount; see [Budget Alerts](./budget-alerts.mdx).
94
96
 
95
97
  ## Editing an App
96
98
 
@@ -42,7 +42,8 @@ After creating a pool, use its tabs to configure it:
42
42
  - **Policy Template**—the pool's [policy template](./policy-templates.mdx), the
43
43
  starting policy chain for apps created in the pool.
44
44
  - **Usage & Limits**—shared budgets for the pool, its sub-pools, and their apps.
45
- See [Usage Limits](./usage-limits.mdx).
45
+ See [Usage Limits](./usage-limits.mdx). Crossing a limit notifies everyone who
46
+ can view the pool; see [Budget Alerts](./budget-alerts.mdx).
46
47
  - **Members**—who belongs to the pool.
47
48
 
48
49
  ## Adding Pool Members
@@ -112,6 +112,10 @@ Add as many rows as you need to cap several meters and periods at once. A Warn
112
112
  row and a Block row on the same meter and period give you warn-then-block—warn
113
113
  at $80, block at $100. The warn amount has to be the lower of the two.
114
114
 
115
+ When usage first crosses a **Warn** or **Block** amount in a period, the people
116
+ who can see the pool or app are notified in the Zuplo Portal and by email. See
117
+ [Budget Alerts](./budget-alerts.mdx).
118
+
115
119
  The **Overview** tab charts daily and monthly usage only. Hourly and weekly rows
116
120
  are enforced, but no meter on that tab tracks them.
117
121
 
@@ -374,3 +378,5 @@ Each app, pool, and the gateway show current usage against their limits:
374
378
  - [Managing Apps](./managing-apps.mdx) - Configure app-level limits
375
379
  - [Fallback Models](./fallback.mdx) - Serve a cheaper model instead of blocking
376
380
  when a limit is exceeded
381
+ - [Budget Alerts](./budget-alerts.mdx) - Who is notified when a **Warn** or
382
+ **Block** amount is crossed, and how to mute alerts
@@ -151,6 +151,8 @@ The **My Keys** tab also shows your allowance and what you've spent against it.
151
151
  If no budget is set, it says your requests have no personal spending limit. Once
152
152
  you reach your allowance, the gateway refuses requests with
153
153
  `429 Too Many Requests` until the period resets or an Admin raises your budget.
154
+ You're notified in the Zuplo Portal and by email when you reach your allowance;
155
+ see [Budget Alerts](./budget-alerts.mdx).
154
156
 
155
157
  ## Removing access
156
158
 
@@ -451,6 +451,7 @@ mutate them.
451
451
 
452
452
  ```ts
453
453
  import {
454
+ AIGatewayModelInfo,
454
455
  AIGatewayModelRouting,
455
456
  AIGatewayModels,
456
457
  ZuploContext,
@@ -461,6 +462,16 @@ interface CheapestModelOptions {
461
462
  providers?: string[];
462
463
  }
463
464
 
465
+ // The model's base input plus output price per token: the rates with no
466
+ // input-size tier. A model without a rate card sorts last.
467
+ function basePrice(model: AIGatewayModelInfo): number {
468
+ const baseRate = (category: "input" | "output") =>
469
+ model.tokenRateCard?.rates.find(
470
+ (rate) => rate.category === category && rate.minInputTokens === undefined
471
+ )?.costPerToken ?? Number.POSITIVE_INFINITY;
472
+ return baseRate("input") + baseRate("output");
473
+ }
474
+
464
475
  export default async function cheapestModel(
465
476
  request: ZuploRequest,
466
477
  context: ZuploContext,
@@ -484,12 +495,7 @@ export default async function cheapestModel(
484
495
  ({ model }) =>
485
496
  model.capability === "completions" && model.status === "active"
486
497
  )
487
- .sort(
488
- (left, right) =>
489
- left.model.inputCostPerToken +
490
- left.model.outputCostPerToken -
491
- (right.model.inputCostPerToken + right.model.outputCostPerToken)
492
- );
498
+ .sort((left, right) => basePrice(left.model) - basePrice(right.model));
493
499
 
494
500
  const cheapest = candidates[0];
495
501
  if (!cheapest) {
@@ -82,7 +82,7 @@ Fallback Model also uses the public routing primitives, so custom code can
82
82
  choose fallbacks dynamically:
83
83
 
84
84
  - `AIGatewayModels.load(context)` returns the cached provider catalog, including
85
- each model's capability, status, and per-token pricing.
85
+ each model's capability, status, and per-token prices (`tokenRateCard`).
86
86
  - `AIGatewayModelRouting.get(context)` returns the sanitized, normalized routing
87
87
  selected earlier in the chain. It never returns provider credentials.
88
88
  - `AIGatewayModelRouting.set(context, routing)` validates every target, resolves
@@ -187,7 +187,7 @@ inbound policy can replace it entirely. The policy's job is to choose one
187
187
  allowed target and store it:
188
188
 
189
189
  - `AIGatewayModels.load(context)` returns the cached provider catalog, including
190
- each model's capability, status, and per-token pricing.
190
+ each model's capability, status, and per-token prices (`tokenRateCard`).
191
191
  - `AIGatewayModelRouting.set(context, routing)` validates the routing, resolves
192
192
  provider credentials internally, and stores the selection that the AI Gateway
193
193
  handler consumes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.12.1",
3
+ "version": "7.12.3",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.12.1",
36
- "@zuplo/core": "7.12.1",
37
- "@zuplo/runtime": "7.12.1",
38
- "@zuplo/test": "7.12.1"
35
+ "@zuplo/cli": "7.12.3",
36
+ "@zuplo/core": "7.12.3",
37
+ "@zuplo/runtime": "7.12.3",
38
+ "@zuplo/test": "7.12.3"
39
39
  }
40
40
  }