zuplo 7.7.1 → 7.7.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.
@@ -56,17 +56,21 @@ is configured, the primary model call runs unbounded.
56
56
 
57
57
  ## Quota fallback
58
58
 
59
- The quota fallback routes requests to an alternate, usually cheaper, model when
60
- an app, team, or gateway [usage limit](./usage-limits.mdx) is exceeded, rather
61
- than blocking the request with a `429`. This keeps an app available after an
62
- applicable budget, token, or request threshold is crossed, while shifting the
63
- overflow traffic to a lower-cost model.
64
-
65
- The **Fallback Model** policy supplies the quota fallback selection. Team and
66
- gateway limits can activate it whether or not the chain includes **Budgets and
67
- Costs**. Add Budgets and Costs when the app also needs its own limits, and place
68
- it after Fallback Model. If you leave the quota fallback empty, the gateway
69
- blocks requests with a `429` once an applicable limit is exceeded.
59
+ For requests continuing to a provider, the quota fallback selects an alternate,
60
+ usually cheaper, model when an app, team, or gateway
61
+ [usage limit](./usage-limits.mdx) is exceeded, rather than blocking the request
62
+ with a `429`. This keeps an app available after an applicable budget, token, or
63
+ request threshold is crossed, while shifting the overflow traffic to a
64
+ lower-cost model.
65
+
66
+ The **Fallback Model** policy supplies the quota fallback selection. Add
67
+ **Budgets and Costs** to set the app's own limits, and place it after Fallback
68
+ Model. If you leave the quota fallback empty, the gateway blocks requests with a
69
+ `429` once an applicable limit is exceeded.
70
+
71
+ If a **Block** budget is exhausted, cache hits return `429` instead of the
72
+ cached answer. Quota fallback doesn't apply to cache hits. See
73
+ [Successful cache responses](./usage-limits.mdx#successful-cache-responses).
70
74
 
71
75
  :::tip
72
76
 
@@ -6,7 +6,7 @@ description:
6
6
  ---
7
7
 
8
8
  Teams in the Zuplo AI Gateway grant users access to apps. Teams are
9
- hierarchical, so access propagates to sub-teams only when the user also has at
9
+ hierarchical, so access propagates to sub-teams only when the user also has
10
10
  permission to view the Zuplo project that contains the AI Gateway. Team or
11
11
  sub-team membership doesn't grant Zuplo project access. Teams are also where you
12
12
  define usage limits and [policy templates](./policy-templates.mdx).
@@ -41,9 +41,8 @@ After creating a team, use its tabs to configure it:
41
41
 
42
42
  - **Policy Template**—the team's [policy template](./policy-templates.mdx), the
43
43
  starting policy chain for apps created in the team.
44
- - **Usage & Limits**—the team's budgets, shared and per-expression, applying to
45
- the team, its sub-teams, and their apps. See
46
- [Usage Limits](./usage-limits.mdx).
44
+ - **Usage & Limits**—shared budgets for the team, its sub-teams, and their apps.
45
+ See [Usage Limits](./usage-limits.mdx).
47
46
  - **Members**—who belongs to the team.
48
47
 
49
48
  ## Adding Team Members
@@ -21,12 +21,12 @@ The chain works in two layers:
21
21
  next production deploy.
22
22
  - **Each app selects from the menu.** An app's chain—which policies run, in what
23
23
  order, with what settings—is edited on the app's **Policies** tab in the
24
- portal. Chain changes apply within about a minute, with no deploy.
24
+ portal. Chain changes apply after cached configuration refreshes, with no
25
+ deploy. See
26
+ [Propagation and accounting delays](./usage-limits.mdx#propagation-and-accounting-delays).
25
27
 
26
28
  No policy is required. An app whose chain is empty still serves requests: each
27
- request names its own model, and no app-specific model restrictions, budgets,
28
- guardrails, or caching apply. Team and gateway usage limits apply regardless of
29
- the app's policy chain.
29
+ request names its own model, and no app-selected policies run.
30
30
 
31
31
  A request passes through whatever the app selected before the AI Gateway handler
32
32
  calls the provider:
@@ -36,6 +36,7 @@ Request
36
36
  → Model Filtering ─┐
37
37
  → Budgets and Costs ├─ the app's policy chain
38
38
  → Custom policy ─┘
39
+ → Applicable budget checks
39
40
  → AI Gateway handler
40
41
  → LLM provider
41
42
  ```
@@ -62,15 +63,17 @@ remove that entry.
62
63
  - Entries run in the listed order, after the gateway's route-level policies and
63
64
  before the AI Gateway handler calls the provider.
64
65
  - A policy can answer the request itself—for example, a semantic cache hit or a
65
- guardrail block. Later entries don't run in that case.
66
+ guardrail block. Later entries don't run in that case. Successful responses
67
+ still pass through automatic budget enforcement; error responses keep their
68
+ original status. See
69
+ [Successful cache responses](./usage-limits.mdx#successful-cache-responses).
66
70
  - An entry can be disabled to keep its configuration without running it.
67
71
  Disabled entries are still validated, so a disabled entry can't reference a
68
72
  policy that's no longer declared.
69
- - An app with an empty chain runs no app-selected policies; requests go straight
70
- to the AI Gateway handler.
71
- - The chain runs on the request path. Policies that need the response—Semantic
72
- Cache and the Akamai AI Firewall—register a response hook from their position
73
- in the chain, so one entry covers both directions.
73
+ - An app with an empty chain runs no app-selected policies. Applicable budget
74
+ checks still run before requests reach the AI Gateway handler.
75
+ - Add Semantic Cache or the Akamai AI Firewall once. Each policy handles both
76
+ incoming requests and responses.
74
77
  - There's no allow-list of policy types. Any policy declared in `policies.json`
75
78
  is a valid chain entry, including a
76
79
  [custom code policy](./custom-policies.mdx). The only exclusions are the
@@ -125,18 +128,17 @@ Model Filtering → Fallback Model → Budgets and Costs → Semantic Cache
125
128
  never creates one. A chain with Fallback Model but no Model Filtering does
126
129
  nothing at all—the policy logs a warning and passes the request through
127
130
  unchanged. That's also why a fallback can never bypass the filter.
128
- - **Budgets and Costs** third: it configures the app's own limits and needs the
129
- resolved selection so an exceeded app limit can activate the quota fallback.
131
+ - **Budgets and Costs** third: use it to set the app's budget and record usage.
132
+ Place it after Fallback Model so an exhausted app budget can use the selected
133
+ quota fallback.
130
134
  - **Semantic Cache** after Budgets and Costs: cache hits still count toward
131
135
  request limits.
132
136
 
133
137
  Tracing policies work well at the end of the chain, so traces reflect the
134
138
  request the earlier policies produced.
135
139
 
136
- Team and gateway limits are enforced outside the app's policy chain. When one is
137
- exceeded, the app's quota fallback works whether or not its chain includes
138
- Budgets and Costs, as long as Model Filtering and Fallback Model supply a
139
- fallback selection.
140
+ When a **Block** budget runs out, requests to a provider can use the app's quota
141
+ fallback. Cache hits return `429` instead of using a fallback.
140
142
 
141
143
  ## Built-in policies
142
144
 
@@ -163,8 +165,9 @@ loader and the executor.
163
165
  Both take a single option, `cacheTtlSeconds`, which sets how long the gateway
164
166
  caches a loaded app configuration. The default is `10` seconds and `2` is the
165
167
  minimum—a lower value throws a configuration error, as does any other option
166
- key. New gateways scaffold the loader with `5`, so configuration changes take
167
- effect within about five seconds.
168
+ key. New gateways scaffold the loader with `5`. This setting controls the
169
+ configuration cache only; authentication caching and budget-decision refreshes
170
+ also affect when a budget change takes effect.
168
171
 
169
172
  ## Authentication
170
173
 
@@ -34,11 +34,10 @@ Policies tab.
34
34
 
35
35
  :::
36
36
 
37
- Everything on the **Policy Template** tab is template content, budgets included:
38
- a **Budgets and Costs** entry in the template sets the default budget each new
39
- app starts with in its own chain, and doesn't enforce anything on the team. The
40
- team's own budgets—shared and per-expression—live on the team's **Usage &
41
- Limits** tab. See [Usage Limits](./usage-limits.mdx).
37
+ New apps inherit their initial budget settings from their team's policy
38
+ template. Configure these settings with **Budgets and Costs** in the template.
39
+ To set a budget shared by all apps in the team, use **Usage & Limits**. See
40
+ [Usage Limits](./usage-limits.mdx).
42
41
 
43
42
  ## Editing a template
44
43
 
@@ -50,10 +49,10 @@ Each policy in the template carries two checkboxes that control what an app
50
49
  starting from the template may do with that entry. Both start checked—a policy
51
50
  you add to a template stays locked until you uncheck the boxes:
52
51
 
53
- | Checkbox | When checked |
54
- | ------------------------------------------------------- | ------------------------------------------------------------------- |
55
- | **Cannot edit** (`canEdit: false` in configuration) | Apps can't change the entry's options—the template's settings apply |
56
- | **Cannot delete** (`canRemove: false` in configuration) | Apps can't remove the entry from their chain |
52
+ | Checkbox | When checked |
53
+ | ----------------- | ------------------------------------------------------------------- |
54
+ | **Cannot edit** | Apps can't change the entry's options—the template's settings apply |
55
+ | **Cannot delete** | Apps can't remove the entry from their chain |
57
56
 
58
57
  Locked entries appear in the app's chain with a lock icon and the note "Locked
59
58
  by the team policy template".
@@ -6,15 +6,16 @@ description:
6
6
  and give every metadata value its own separate budget using expression rules.
7
7
  ---
8
8
 
9
- The Zuplo AI Gateway provides hierarchical usage limits and budget controls to
10
- manage LLM spending across the gateway's Zuplo project. Limits can be set at the
11
- gateway, team, and app levels.
9
+ Set spending, token, and request budgets for your apps, teams, and gateway to
10
+ control how much they can use.
12
11
 
13
12
  ## Budget Hierarchy
14
13
 
15
- Limits at every level apply together. A request is blocked when the gateway, an
16
- ancestor team, or the app is over a **Block** limit. The error identifies
17
- whether a shared node budget or an expression-specific budget ran out:
14
+ Limits at every level apply together. The gateway checks its own limits, parent
15
+ team limits, and app limits. An exhausted **Block** limit rejects the request
16
+ unless the normal provider path uses a configured
17
+ [quota fallback](#when-a-limit-is-exceeded). **Warn** limits allow the request
18
+ and report the warning. Budgets cover these levels:
18
19
 
19
20
  - **Gateway** - Limits across the Zuplo project (for example, $1,000/day),
20
21
  covering all teams, sub-teams, and apps combined
@@ -23,83 +24,76 @@ whether a shared node budget or an expression-specific budget ran out:
23
24
  - **Apps** - Per-app limits for granular control (for example, $10/day for a
24
25
  hackathon app)
25
26
 
26
- Budgets at different levels don't constrain each other's values, and app budgets
27
- under a team may add up to more than the team's own budget - the team's budget
28
- is the aggregate cap. For example, take a $100/month team containing a $50/month
29
- sub-team with two apps capped at $25/month and $40/month. When the apps have
30
- spent $40 and $10, the sub-team's aggregate reaches $50 and requests through
31
- **both** apps are blocked - including the app that is still under its own $25
32
- cap. Apps elsewhere in the $100/month team keep working until that team's own
33
- aggregate reaches $100.
34
-
35
- <Diagram direction="vertical" height="h-96">
36
- <DiagramNode id="team" variant="green">
37
- Team · spent $50 of $100
27
+ For example, a team has a $50 monthly shared **Block** budget. App A spends $40
28
+ and App B spends $10. Once that usage is reflected in the budget check, both
29
+ apps have reached the team's budget. Without a quota fallback, requests through
30
+ both apps receive `429 Too Many Requests`. An app outside this team doesn't
31
+ share its $50 budget, but still has its own app, team, and gateway limits.
32
+
33
+ <Diagram direction="vertical" height="h-80">
34
+ <DiagramNode id="gateway" variant="green">
35
+ Gateway · shared budget available
38
36
  </DiagramNode>
39
- <DiagramNode id="subteam" variant="red">
40
- Sub-team · spent $50 of $50
37
+ <DiagramNode id="team" variant="red">
38
+ Team · spent $50 of $50 this month
41
39
  </DiagramNode>
42
40
  <DiagramNode id="appa" variant="red">
43
- App A · spent $10 of $25
41
+ App A · spent $40
44
42
  </DiagramNode>
45
43
  <DiagramNode id="appb" variant="red">
46
- App B · spent $40 of $40
44
+ App B · spent $10
47
45
  </DiagramNode>
48
46
  <DiagramNode id="other" variant="green">
49
- Other apps in the team
47
+ App outside the team
50
48
  </DiagramNode>
51
- <DiagramEdge from="team" to="subteam" label="at its Block limit" />
52
- <DiagramEdge from="team" to="other" label="still serving" />
53
- <DiagramEdge from="subteam" to="appa" label="blocked — under its own cap" />
54
- <DiagramEdge from="subteam" to="appb" label="blocked" />
49
+ <DiagramEdge from="gateway" to="team" label="team budget exhausted" />
50
+ <DiagramEdge
51
+ from="gateway"
52
+ to="other"
53
+ label="unaffected by this team budget"
54
+ />
55
+ <DiagramEdge from="team" to="appa" label="429 without quota fallback" />
56
+ <DiagramEdge from="team" to="appb" label="429 without quota fallback" />
55
57
  </Diagram>
56
58
 
59
+ App budgets can add up to more than their team's budget. The team's budget caps
60
+ the combined usage; increasing an app's budget doesn't increase its team's cap.
61
+ The same rule applies to nested teams and the gateway root.
62
+
57
63
  ## Where limits are configured
58
64
 
59
- | Level | Shared budget | Per-expression budgets |
60
- | ------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------ |
61
- | Gateway | **Settings → Usage Limits** | Supported in configuration, but not authorable in the Portal |
62
- | Team | The team's **Usage & Limits** tab | The team's **Usage & Limits** tab |
63
- | App | The **Budgets and Costs** policy in the app's [policy chain](./policy-chains.mdx) | The **Budgets and Costs** policy |
65
+ | Level | Budgets | Where to configure |
66
+ | ------- | ----------------------- | ---------------------------------------------------------------------- |
67
+ | Gateway | Shared | **Settings → Usage Limits** |
68
+ | Team | Shared | The team's **Usage & Limits** tab |
69
+ | App | Shared and per-metadata | **Budgets and Costs** in the app's [policy chain](./policy-chains.mdx) |
64
70
 
65
- Gateway and team limits apply independently to every descendant app; they aren't
66
- copied into the app's configuration. Each node meters its own usage plus all
67
- descendant usage. A request is blocked if any node in its path is over a
68
- **Block** limit. An app's own limits live on the Budgets and Costs policy.
71
+ Use **Budgets and Costs** to set an app's own budget.
69
72
 
70
73
  :::note
71
74
 
72
- Everything a team enforces lives on its **Usage & Limits** tab. The **Policy
73
- Template** tab is only the template: a Budgets and Costs entry there is copied
74
- into each new app as the app's own starting budget and doesn't enforce the
75
- team's budget.
75
+ New apps inherit their initial budget settings from their
76
+ [team policy template](./policy-templates.mdx). To limit the team's combined
77
+ usage, set a budget on **Usage & Limits**.
76
78
 
77
79
  :::
78
80
 
79
81
  ## How a budget rule works
80
82
 
81
- Every node's budgets are a list of rules. Each rule answers two questions—what
82
- it budgets, and what the caps are.
83
+ Each budget rule defines who shares the budget and how much they can use.
83
84
 
84
85
  **Scope** decides who shares the budget:
85
86
 
86
- | Scope | What it budgets |
87
- | ------------------------------------------ | ------------------------------------------------------------------------ |
88
- | **Shared** (`budgetBy: "app"`) | One shared budget for the gateway, team, or app and its descendants |
89
- | **By metadata** (`budgetBy: "expression"`) | A separate budget at that node for every distinct value of an expression |
87
+ | Scope | What it budgets |
88
+ | --------------- | ----------------------------------------------------------- |
89
+ | **Shared** | One budget for the app, team, or gateway |
90
+ | **By metadata** | A separate budget for every distinct value of an expression |
90
91
 
91
92
  A **By metadata** rule gives each distinct expression value its own allowance.
92
93
  With the expression `request.headers.get("x-user")` and a $5 daily cap, every
93
94
  distinct `x-user` value gets its own $5 per day—one user exhausting their budget
94
- doesn't affect anyone else. A node can combine a shared rule with several
95
- expression rules.
96
-
97
- Ancestor rules of both kinds apply to every request through the node's subtree.
98
- The difference is who gets blocked: a team's **Shared** rule blocks all of the
99
- team's traffic once the team's aggregate is over, while a team's **By metadata**
100
- rule meters each value across all of the team's apps combined and blocks only
101
- the exhausted value—the same `x-user` shares one budget whether it calls App A
102
- or App B.
95
+ doesn't affect anyone else. An app can combine a shared budget with several
96
+ per-metadata budgets.
103
97
 
104
98
  **Limit rows** set the caps. Each row is a meter, a period, an amount, and an
105
99
  action:
@@ -142,14 +136,14 @@ reset the period's usage. Custom anchors aren't configurable.
142
136
  the meters and periods you want to cap. For a **By metadata** rule, enter the
143
137
  expression that identifies each caller.
144
138
 
145
- 1. Select **Apply**, then **Save changes** on the policy chain. The change
146
- applies within about a minute.
139
+ 1. Select **Apply**, then **Save changes** on the policy chain. See
140
+ [Propagation and accounting delays](#propagation-and-accounting-delays) for
141
+ when the change takes effect.
147
142
 
148
143
  </Stepper>
149
144
 
150
- The app editor groups gateway and ancestor team rules under **Inherited**. Those
151
- rules remain stored and enforced at the gateway or team; edit them where they
152
- were created.
145
+ The app editor shows team and gateway budgets under **Inherited**. To change
146
+ one, open the team or gateway where you set it.
153
147
 
154
148
  ### Budget expressions
155
149
 
@@ -180,8 +174,8 @@ but budget separately.
180
174
 
181
175
  :::
182
176
 
183
- App rules are stored in `options.budgetRules` on the app's own Budgets and Costs
184
- policy entry. For the app rule schema, see the
177
+ To configure budgets in JSON, use the `budgetRules` option in **Budgets and
178
+ Costs**. For the available options, see the
185
179
  [Budgets and Costs policy reference](../policies/ai-gateway-metering-v2-inbound.mdx).
186
180
 
187
181
  This rule set budgets each user, each organization, and the app as a whole:
@@ -236,37 +230,29 @@ month, warning at $2,000, resetting on the first of the month.
236
230
 
237
231
  ## Setting team and gateway limits
238
232
 
239
- Team and gateway limits cover the node's own usage and all descendant usage.
240
- They apply to every descendant app whether or not its chain includes Budgets and
241
- Costs.
233
+ A team budget covers the combined usage of its apps and sub-teams. A gateway
234
+ budget covers all apps in the project.
242
235
 
243
- Team rules—shared and per-expression—are added on the team's **Usage & Limits**
244
- tab, using the same meter, period, amount, and action rows as the app editor.
236
+ To add a shared team rule, select the team or sub-team in
237
+ [Apps & Teams](https://portal.zuplo.com/+/account/project/ai/teams), then open
238
+ **Usage & Limits**. To add a shared gateway rule, open **Settings → Usage
239
+ Limits**. Both editors use the same meter, period, amount, and action rows as
240
+ the app editor.
245
241
 
246
- To add a shared gateway rule, open **Settings Usage Limits**. The gateway root
247
- supports per-expression rules in `metadata.budgetRules`, but the Portal doesn't
248
- author them. It displays a stored expression rule as read-only and lets you
249
- delete it.
250
-
251
- Each `budgetBy: "app"` rule is a shared budget for the node where the rule is
252
- stored. At the gateway or team level, `app` means that gateway or team node, not
253
- an app policy configuration. A team's usage includes its sub-teams and apps, and
254
- the gateway's usage includes the whole project.
255
-
256
- Gateway and team rules are stored in `metadata.budgetRules` on that node. The
257
- editor groups ancestor rules under **Inherited**, but each rule remains enforced
258
- at the node where it was created.
242
+ The editor shows budgets from parent teams and the gateway under **Inherited**.
243
+ To change an inherited budget, edit it on the team or gateway where you created
244
+ it.
259
245
 
260
246
  ## When a limit is exceeded
261
247
 
262
- If the app has a **quota fallback** model configured, an exceeded limit routes
263
- requests to that model instead of blocking—see
248
+ For a request continuing to a provider, an exhausted **Block** limit uses the
249
+ app's configured **quota fallback** model instead of blocking. See
264
250
  [Fallback Models](./fallback.mdx), where the fallback model is selected. This
265
251
  applies to gateway and team limits as well as the app's own. The fallback's
266
252
  usage still counts toward the limits.
267
253
 
268
- Without a fallback, the request is rejected with `429 Too Many Requests` and a
269
- `budget` object naming what ran out:
254
+ Without a quota fallback on that path, the request is rejected with
255
+ `429 Too Many Requests` and a `budget` object naming what ran out:
270
256
 
271
257
  ```json
272
258
  {
@@ -285,20 +271,44 @@ Without a fallback, the request is rejected with `429 Too Many Requests` and a
285
271
  ```
286
272
 
287
273
  The `scope` tells the caller which budget they hit. It is `application` when the
288
- gateway, a team, or the app exhausts a shared node budget. It is `dimension`
289
- when a value exhausts an expression budget. Quote the `ruleId` when raising a
290
- support request.
274
+ gateway, a team, or the app exhausts a shared budget. It is `dimension` when a
275
+ value exhausts an expression budget. Quote the `ruleId` when raising a support
276
+ request.
277
+
278
+ ### Successful cache responses
279
+
280
+ Cached responses are subject to budget limits too. If a **Block** budget is
281
+ exhausted, a cache hit returns `429 Too Many Requests` instead of the cached
282
+ answer. Quota fallback doesn't apply to cache hits. **Warn** limits allow the
283
+ cached answer through.
284
+
285
+ Authentication failures and other error responses keep their original response;
286
+ the budget check doesn't replace an authentication error with a budget error.
291
287
 
292
288
  :::caution{title="Budgets fail open by default"}
293
289
 
294
- The Budgets and Costs policy's `throwOnFailure` option—**Fail closed when
295
- metering is unavailable** in the editor—controls how an error while checking the
296
- app's own limits is handled. It defaults to `false`, so the request continues
297
- through the gateway. Set it to `true` to reject the request instead. Team and
298
- gateway limit checks run independently of this policy.
290
+ If a team or gateway budget check fails, the request can continue. However, a
291
+ failed check doesn't override a budget violation already detected: a cache hit
292
+ still returns `429` if the app's budget check has found an exhausted **Block**
293
+ limit.
294
+
295
+ To reject requests when **Budgets and Costs** can't complete its own checks,
296
+ enable **Fail closed when metering is unavailable** (`throwOnFailure`). This
297
+ option is off by default and doesn't change how team or gateway check failures
298
+ are handled.
299
299
 
300
300
  :::
301
301
 
302
+ ## Propagation and accounting delays
303
+
304
+ Budget changes don't take effect immediately. After editing a budget or moving
305
+ an app to another team, allow time for the saved settings and usage totals to
306
+ update. The delay depends on your gateway's
307
+ [cache settings](./policy-chains.mdx#configuration-executor).
308
+
309
+ Requests already in progress and delays in updating usage totals can take
310
+ spending past a budget before further requests are blocked.
311
+
302
312
  ## Monitoring Usage
303
313
 
304
314
  Each app, team, and the gateway show current usage against their limits:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.7.1",
3
+ "version": "7.7.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.7.1",
36
- "@zuplo/core": "7.7.1",
37
- "@zuplo/runtime": "7.7.1",
38
- "@zuplo/test": "7.7.1"
35
+ "@zuplo/cli": "7.7.3",
36
+ "@zuplo/core": "7.7.3",
37
+ "@zuplo/runtime": "7.7.3",
38
+ "@zuplo/test": "7.7.3"
39
39
  }
40
40
  }