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.
- package/docs/ai-gateway/budget-alerts.mdx +118 -0
- package/docs/ai-gateway/cookbooks/dynamic-model-routing.mdx +30 -10
- package/docs/ai-gateway/managing-apps.mdx +3 -1
- package/docs/ai-gateway/managing-pools.mdx +2 -1
- package/docs/ai-gateway/usage-limits.mdx +6 -0
- package/docs/ai-gateway/user-apps.mdx +2 -0
- package/docs/policies/ai-gateway-configuration-executor-inbound/doc.md +12 -6
- package/docs/policies/ai-gateway-fallback-model-inbound/doc.md +1 -1
- package/docs/policies/ai-gateway-model-filtering-inbound/doc.md +1 -1
- package/package.json +5 -5
|
@@ -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
|
|
20
|
-
|
|
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
|
-
({
|
|
56
|
-
model.capability === "completions" &&
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
36
|
-
"@zuplo/core": "7.12.
|
|
37
|
-
"@zuplo/runtime": "7.12.
|
|
38
|
-
"@zuplo/test": "7.12.
|
|
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
|
}
|