@limitry/sdk 1.0.0-rc.7 → 1.0.0-rc.9
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/CHANGELOG.md +122 -0
- package/README.md +15 -13
- package/dist/index.d.ts +35 -32
- package/dist/index.js +2 -2
- package/dist/product/index.d.ts +4 -4
- package/dist/product/index.js +7 -3
- package/dist/product/limits.d.ts +72 -45
- package/dist/product/limits.js +139 -62
- package/dist/resources.gen.d.ts +35 -32
- package/dist/resources.gen.js +7 -7
- package/dist/retry.d.ts +38 -0
- package/dist/retry.js +85 -1
- package/dist/v1.gen.d.ts +202 -171
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,128 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in the Limitry TypeScript SDK (`@limitry/sdk`). Generated from the source on each release; newest first.
|
|
4
4
|
|
|
5
|
+
## 1.0.0-rc.9 (2026-10-22)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `POST /v1/checks` can now answer `503` with the code `busy` when
|
|
10
|
+
Limitry is deciding too many checks for those subjects at once. Nothing
|
|
11
|
+
was charged. `guard` and `reserve` (TypeScript and Python) send the
|
|
12
|
+
check again with the same key, within the same 30 seconds they give
|
|
13
|
+
`moving`, before the limit's fail mode decides. Each retry of `busy` or
|
|
14
|
+
`moving` now waits the answer's `Retry-After` plus a random backoff that
|
|
15
|
+
grows with each try, so clients don't all come back at the same moment.
|
|
16
|
+
Older SDK versions treat `busy` as Limitry being unreachable after their
|
|
17
|
+
usual two quick retries, so the fail mode decides; upgrade to retry it.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- A very busy workspace's counts are now spread over more of Limitry's
|
|
22
|
+
servers as its traffic grows. While some of them move, which takes a few
|
|
23
|
+
seconds, checks, commits and releases on those subjects answer `503`
|
|
24
|
+
with the new error code `moving` and `Retry-After: 1`; nothing is
|
|
25
|
+
counted. `guard` and `reserve` (TypeScript and Python) and the CLI send
|
|
26
|
+
the same request again with the same Idempotency-Key for up to 30
|
|
27
|
+
seconds, each try with its own time budget, before a limit's fail mode
|
|
28
|
+
applies, so work is neither run uncounted nor left uncommitted.
|
|
29
|
+
- When `guard` gets no answer to its check in time and the limit fails
|
|
30
|
+
open (so your work runs), it now sends the same check once more in the
|
|
31
|
+
background, with the same Idempotency-Key and the header
|
|
32
|
+
`limitry-finish-only: 1`: Limitry finishes a check it was part way
|
|
33
|
+
through, and never decides one afresh, so a check that never arrived
|
|
34
|
+
charges nothing. A limit that fails closed sends nothing more.
|
|
35
|
+
|
|
36
|
+
`POST /v1/checks` answers `204` (no body) to such a resend when there was
|
|
37
|
+
nothing to finish, so `checks.create` is typed to allow no decision; a
|
|
38
|
+
check sent without `limitry-finish-only` always gets one.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A reservation (`mode: "reserve"`) now starts a subject's period, as a
|
|
43
|
+
plain check does: on a `billing_date` limit with no billing date set
|
|
44
|
+
yet, the first reservation's time becomes the subject's billing date,
|
|
45
|
+
and on a `first_use` limit it starts the period. Before, reservations
|
|
46
|
+
made before the subject's first commit could each count in a period of
|
|
47
|
+
their own, so a monthly limit could let more through than its amount.
|
|
48
|
+
A reservation you release still starts the period, with nothing used in
|
|
49
|
+
it.
|
|
50
|
+
- A `503 moving` answer with `Retry-After: 0` is now retried after at least
|
|
51
|
+
a second, not at once, and still for at most 30 seconds. A client made
|
|
52
|
+
with `maxRetries: 0` no longer sends the SDK's internal
|
|
53
|
+
`x-limitry-moving` header to the API.
|
|
54
|
+
|
|
55
|
+
## 1.0.0-rc.8 (2026-10-21)
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Limits have `counting`: `fast` answers the instant it decides, and
|
|
60
|
+
`guaranteed` stores every count safely before answering, so the limit
|
|
61
|
+
never goes over, even if a server fails. A `total` balance defaults to
|
|
62
|
+
`guaranteed`, every other window to `fast`. Set it when creating or
|
|
63
|
+
updating a limit (`counting`, `--counting`), or in `limitry.json`.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- - Creating a limit needs only who it is for, the amount and the window:
|
|
68
|
+
`limits.create({ subject: { kind: "user" }, amount: 100, window: "month" })`
|
|
69
|
+
(`limitry limits create --subject '{"kind":"user"}' --amount 100 --window month`).
|
|
70
|
+
`action` defaults to every action (`*`), `unit` to `units`, and `name`
|
|
71
|
+
to the sentence the limit reads as, such as "100 units per user per
|
|
72
|
+
month". If a limit already has that name, the API answers `409`: give
|
|
73
|
+
this one a `name`. `limitry.json` entries still need a `name`.
|
|
74
|
+
- A limit's input names who it is for as `subject: { kind, id }` (was
|
|
75
|
+
`subjectKind` and `subjectId`), the same shape the limit itself has.
|
|
76
|
+
- What a limit counts is fixed once it is created: its subject, action,
|
|
77
|
+
unit, window, `every`, `start` and `rolling`. Changing one answers
|
|
78
|
+
`400 invalid_request`: create a new limit and archive the old one (its
|
|
79
|
+
history stays). Name, amount, fail mode and enabled still change any
|
|
80
|
+
time. `limitry sync` refuses such a change until you rename the entry.
|
|
81
|
+
- A limit says when a check last reached it (`lastCheckedAt`), so you can
|
|
82
|
+
see a limit no check names.
|
|
83
|
+
- One name for each idea, everywhere: the API, both SDKs, the CLI, the MCP
|
|
84
|
+
tools, `limitry.json` and webhooks. Update your code when you upgrade:
|
|
85
|
+
|
|
86
|
+
- `hold()` is `reserve()`: it takes `cost` (was `estimate`), and the block
|
|
87
|
+
gets a `Reservation` (was `Held`; Python `Reservation` and
|
|
88
|
+
`AsyncReservation`), with the same `cost()` and `extend()`.
|
|
89
|
+
- A refusal throws `LimitReached` (was `LimitExceeded`), with `limitId`
|
|
90
|
+
(Python `limit_id`) and `reason` `limit_reached` or
|
|
91
|
+
`too_many_reservations`. When Limitry can't be reached and a limit fails
|
|
92
|
+
closed, you now get a separate `Unavailable` error. "Can't be reached"
|
|
93
|
+
also covers a `402 plan_limit_reached` and a `429` from Limitry's flood
|
|
94
|
+
guard, so each limit's fail mode decides instead of your code getting an
|
|
95
|
+
error.
|
|
96
|
+
- A check's answer lists its limits in `limits` (was `remaining`): read
|
|
97
|
+
`decision.limits[0].remaining`. Every answer that names a limit says
|
|
98
|
+
`limitId`: `reason.limitId`, `limits[].limitId`, `usage.standing`'s
|
|
99
|
+
`limits[].limitId`, and webhook payloads.
|
|
100
|
+
- The refusal code is `limit_reached` (was `limit_exceeded`), and the
|
|
101
|
+
webhook event is `limit.reached` (was `limit.exceeded`): subscribe your
|
|
102
|
+
endpoints to the new name.
|
|
103
|
+
- Usage says `refused` (was `denied`); `usage.subjects` no longer has
|
|
104
|
+
`over` (it is `refused > 0`).
|
|
105
|
+
- Credits: `limits.credits.create()` and `limits.credits.list()` at
|
|
106
|
+
`/v1/limits/{limitId}/credits` (were `limits.grants.*` at `…/grants`),
|
|
107
|
+
ids start `crd_` (were `grt_`), and the approval an agent asks for is
|
|
108
|
+
`limit.credit` (was `limit.grant`).
|
|
109
|
+
- A period that starts on the subject's billing date is
|
|
110
|
+
`start: "billing_date"` (was `"anchor"`), and `subjects.update()` takes
|
|
111
|
+
`billingDate` (was `anchor`); the subject says `billingDate` too.
|
|
112
|
+
- `usage.standing()` and `usage.query()` take one `subject`, written
|
|
113
|
+
`kind:id` (`user:usr_123`), instead of separate kind and id fields
|
|
114
|
+
(`limitry usage standing --subject user:usr_123`).
|
|
115
|
+
- A limit's id starts `lmt_` (was `lim_`); keys still start `lim_test_`
|
|
116
|
+
and `lim_live_`.
|
|
117
|
+
- `limitry sync --preview` (was `--dry-run`), and the API's `preview`
|
|
118
|
+
(was `dryRun`), in what you send and what comes back.
|
|
119
|
+
|
|
120
|
+
### Removed
|
|
121
|
+
|
|
122
|
+
- Limits no longer have `counting` (Fast or Guaranteed), and the CLI no
|
|
123
|
+
longer has `--counting`: every count is now saved before Limitry answers,
|
|
124
|
+
for every limit. Remove `counting` from your code and from
|
|
125
|
+
`limitry.json`; Limitry ignores it.
|
|
126
|
+
|
|
5
127
|
## 1.0.0-rc.7 (2026-10-19)
|
|
6
128
|
|
|
7
129
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @limitry/sdk
|
|
2
2
|
|
|
3
|
-
The Limitry TypeScript SDK puts a limit around costly work in one call: `guard` checks before your code runs, and `
|
|
3
|
+
The Limitry TypeScript SDK puts a limit around costly work in one call: `guard` checks before your code runs, and `reserve` reserves what the work will probably cost, then charges what it actually cost. Every API operation is a typed method too.
|
|
4
4
|
|
|
5
5
|
Limitry is the limits, credits and budgets layer for your software: give each of your users, teams, tenants, API keys or agents an allowance, and before any costly action one call tells your code whether it may happen.
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ key (`lim_test_…`) while you build.
|
|
|
19
19
|
it counts one AI call against a limit such as 100 a day:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
import { createClient,
|
|
22
|
+
import { createClient, LimitReached } from "@limitry/sdk";
|
|
23
23
|
|
|
24
24
|
const limitry = createClient(); // reads LIMITRY_API_KEY
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ try {
|
|
|
29
29
|
() => callModel(prompt),
|
|
30
30
|
);
|
|
31
31
|
} catch (error) {
|
|
32
|
-
if (error instanceof
|
|
32
|
+
if (error instanceof LimitReached) {
|
|
33
33
|
console.log(`Try again in ${error.retryAfterSeconds} seconds`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -38,26 +38,28 @@ try {
|
|
|
38
38
|
## Charge what it actually cost
|
|
39
39
|
|
|
40
40
|
When you only know the cost afterwards, such as an AI reply priced by
|
|
41
|
-
the token, `
|
|
42
|
-
and charges what it used. If your code throws, the
|
|
41
|
+
the token, `reserve` reserves what it will probably cost (5 cents here),
|
|
42
|
+
runs your code and charges what it used. If your code throws, the
|
|
43
|
+
reservation is released:
|
|
43
44
|
|
|
44
45
|
```ts
|
|
45
46
|
import { createClient } from "@limitry/sdk";
|
|
46
47
|
|
|
47
48
|
const limitry = createClient();
|
|
48
49
|
|
|
49
|
-
const answer = await limitry.
|
|
50
|
-
{ subject: { kind: "user", id: "usr_123" }, action: "ai-call",
|
|
51
|
-
async (
|
|
50
|
+
const answer = await limitry.reserve(
|
|
51
|
+
{ subject: { kind: "user", id: "usr_123" }, action: "ai-call", cost: 5 },
|
|
52
|
+
async (reservation) => {
|
|
52
53
|
const response = await callModel(prompt);
|
|
53
|
-
|
|
54
|
+
reservation.cost(priceCents(response.usage)); // your prices, in cents
|
|
54
55
|
return response;
|
|
55
56
|
},
|
|
56
57
|
);
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
If Limitry can't be reached, each
|
|
60
|
-
the work run, `closed`
|
|
60
|
+
A refusal throws `LimitReached`. If Limitry can't be reached, each
|
|
61
|
+
limit's fail mode decides: `open` lets the work run, `closed` throws
|
|
62
|
+
`Unavailable`.
|
|
61
63
|
|
|
62
64
|
## Everything else
|
|
63
65
|
|
|
@@ -69,9 +71,9 @@ import { createClient } from "@limitry/sdk";
|
|
|
69
71
|
|
|
70
72
|
const limitry = createClient();
|
|
71
73
|
|
|
74
|
+
// "100 units per user per day for ai-call"
|
|
72
75
|
await limitry.limits.create({
|
|
73
|
-
|
|
74
|
-
subjectKind: "user",
|
|
76
|
+
subject: { kind: "user" },
|
|
75
77
|
action: "ai-call",
|
|
76
78
|
amount: 100,
|
|
77
79
|
window: "day",
|
package/dist/index.d.ts
CHANGED
|
@@ -76,18 +76,18 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
76
76
|
create: (input: import("./resources.ts").Input<"/checks", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
77
77
|
allowed: boolean;
|
|
78
78
|
id: string;
|
|
79
|
+
limits: components["schemas"]["CheckLimit"][];
|
|
79
80
|
reason: {
|
|
80
|
-
code: "
|
|
81
|
-
|
|
81
|
+
code: "limit_reached" | "too_many_reservations";
|
|
82
|
+
limitId: string | null;
|
|
82
83
|
message: string;
|
|
83
84
|
} | null;
|
|
84
|
-
remaining: components["schemas"]["CheckRemaining"][];
|
|
85
85
|
reservation?: {
|
|
86
86
|
expiresAt: string;
|
|
87
87
|
id: string;
|
|
88
88
|
};
|
|
89
89
|
retryAfterSeconds: number | null;
|
|
90
|
-
}>;
|
|
90
|
+
} | undefined>;
|
|
91
91
|
};
|
|
92
92
|
limits: {
|
|
93
93
|
archive: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<undefined>;
|
|
@@ -100,15 +100,33 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
100
100
|
every: number;
|
|
101
101
|
failMode: "open" | "closed";
|
|
102
102
|
id: string;
|
|
103
|
+
lastCheckedAt: string | null;
|
|
103
104
|
name: string;
|
|
104
105
|
rolling: boolean;
|
|
105
|
-
start: "calendar" | "
|
|
106
|
+
start: "calendar" | "billing_date" | "first_use";
|
|
106
107
|
subject: components["schemas"]["LimitSubject"];
|
|
107
108
|
unit: string;
|
|
108
109
|
updatedAt: string;
|
|
109
110
|
version: number;
|
|
110
111
|
window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
|
|
111
112
|
}>;
|
|
113
|
+
credits: {
|
|
114
|
+
create: (limitId: string | number, input: import("./resources.ts").Input<"/limits/{limitId}/credits", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
115
|
+
amount: number;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
createdByUserId: string | null;
|
|
118
|
+
id: string;
|
|
119
|
+
limitId: string;
|
|
120
|
+
note: string;
|
|
121
|
+
subject: {
|
|
122
|
+
id: string;
|
|
123
|
+
kind: string;
|
|
124
|
+
};
|
|
125
|
+
}>;
|
|
126
|
+
list: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
127
|
+
items: components["schemas"]["Credit"][];
|
|
128
|
+
}>;
|
|
129
|
+
};
|
|
112
130
|
get: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
113
131
|
action: string;
|
|
114
132
|
amount: number;
|
|
@@ -118,32 +136,16 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
118
136
|
every: number;
|
|
119
137
|
failMode: "open" | "closed";
|
|
120
138
|
id: string;
|
|
139
|
+
lastCheckedAt: string | null;
|
|
121
140
|
name: string;
|
|
122
141
|
rolling: boolean;
|
|
123
|
-
start: "calendar" | "
|
|
142
|
+
start: "calendar" | "billing_date" | "first_use";
|
|
124
143
|
subject: components["schemas"]["LimitSubject"];
|
|
125
144
|
unit: string;
|
|
126
145
|
updatedAt: string;
|
|
127
146
|
version: number;
|
|
128
147
|
window: "minute" | "hour" | "day" | "week" | "month" | "year" | "total" | "concurrent";
|
|
129
148
|
}>;
|
|
130
|
-
grants: {
|
|
131
|
-
create: (limitId: string | number, input: import("./resources.ts").Input<"/limits/{limitId}/grants", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
132
|
-
amount: number;
|
|
133
|
-
createdAt: string;
|
|
134
|
-
createdByUserId: string | null;
|
|
135
|
-
id: string;
|
|
136
|
-
limitId: string;
|
|
137
|
-
note: string;
|
|
138
|
-
subject: {
|
|
139
|
-
id: string;
|
|
140
|
-
kind: string;
|
|
141
|
-
};
|
|
142
|
-
}>;
|
|
143
|
-
list: (limitId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
144
|
-
items: components["schemas"]["LimitGrant"][];
|
|
145
|
-
}>;
|
|
146
|
-
};
|
|
147
149
|
list: (input?: import("./resources.ts").Input<"/limits", "get">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
148
150
|
items: components["schemas"]["Limit"][];
|
|
149
151
|
nextCursor: string | null;
|
|
@@ -157,9 +159,10 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
157
159
|
every: number;
|
|
158
160
|
failMode: "open" | "closed";
|
|
159
161
|
id: string;
|
|
162
|
+
lastCheckedAt: string | null;
|
|
160
163
|
name: string;
|
|
161
164
|
rolling: boolean;
|
|
162
|
-
start: "calendar" | "
|
|
165
|
+
start: "calendar" | "billing_date" | "first_use";
|
|
163
166
|
subject: components["schemas"]["LimitSubject"];
|
|
164
167
|
unit: string;
|
|
165
168
|
updatedAt: string;
|
|
@@ -169,7 +172,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
169
172
|
sync: (input: import("./resources.ts").Input<"/limits/sync", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
170
173
|
archived: string[];
|
|
171
174
|
created: string[];
|
|
172
|
-
|
|
175
|
+
preview: boolean;
|
|
173
176
|
unchanged: string[];
|
|
174
177
|
updated: {
|
|
175
178
|
changes: {
|
|
@@ -190,9 +193,10 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
190
193
|
every: number;
|
|
191
194
|
failMode: "open" | "closed";
|
|
192
195
|
id: string;
|
|
196
|
+
lastCheckedAt: string | null;
|
|
193
197
|
name: string;
|
|
194
198
|
rolling: boolean;
|
|
195
|
-
start: "calendar" | "
|
|
199
|
+
start: "calendar" | "billing_date" | "first_use";
|
|
196
200
|
subject: components["schemas"]["LimitSubject"];
|
|
197
201
|
unit: string;
|
|
198
202
|
updatedAt: string;
|
|
@@ -225,7 +229,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
225
229
|
};
|
|
226
230
|
subjects: {
|
|
227
231
|
get: (subjectKind: string | number, subjectId: string | number, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
228
|
-
|
|
232
|
+
billingDate: string | null;
|
|
229
233
|
subject: components["schemas"]["SubjectRef"];
|
|
230
234
|
}>;
|
|
231
235
|
reset: (subjectKind: string | number, subjectId: string | number, input?: import("./resources.ts").Input<"/subjects/{subjectKind}/{subjectId}/reset", "post">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
@@ -233,7 +237,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
233
237
|
subject: components["schemas"]["SubjectRef"];
|
|
234
238
|
}>;
|
|
235
239
|
update: (subjectKind: string | number, subjectId: string | number, input: import("./resources.ts").Input<"/subjects/{subjectKind}/{subjectId}", "patch">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
236
|
-
|
|
240
|
+
billingDate: string | null;
|
|
237
241
|
subject: components["schemas"]["SubjectRef"];
|
|
238
242
|
}>;
|
|
239
243
|
};
|
|
@@ -243,7 +247,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
243
247
|
limitId: string;
|
|
244
248
|
points: {
|
|
245
249
|
allowed: number;
|
|
246
|
-
|
|
250
|
+
refused: number;
|
|
247
251
|
start: string;
|
|
248
252
|
used: number;
|
|
249
253
|
}[];
|
|
@@ -253,7 +257,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
253
257
|
limits: {
|
|
254
258
|
action: string;
|
|
255
259
|
amount: number;
|
|
256
|
-
|
|
260
|
+
limitId: string;
|
|
257
261
|
name: string;
|
|
258
262
|
remaining: number;
|
|
259
263
|
resetsAt: string | null;
|
|
@@ -274,8 +278,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
|
|
|
274
278
|
}>;
|
|
275
279
|
subjects: (input: import("./resources.ts").Input<"/usage/subjects", "get">, options?: import("./resources.ts").RequestOptions) => Promise<{
|
|
276
280
|
items: {
|
|
277
|
-
|
|
278
|
-
over: boolean;
|
|
281
|
+
refused: number;
|
|
279
282
|
remaining: number;
|
|
280
283
|
subject: {
|
|
281
284
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import createOpenApiClient from "openapi-fetch";
|
|
|
3
3
|
import { apiErrorFrom } from "./errors.js";
|
|
4
4
|
import * as product from "./product/index.js";
|
|
5
5
|
import { resources } from "./resources.gen.js";
|
|
6
|
-
import { withRetries } from "./retry.js";
|
|
6
|
+
import { withoutMovingHeader, withRetries } from "./retry.js";
|
|
7
7
|
import { verifyWebhook } from "./webhooks.js";
|
|
8
8
|
export { ApiError, apiErrorFrom } from "./errors.js";
|
|
9
9
|
export { verifyWebhook, WebhookVerificationError, } from "./webhooks.js";
|
|
@@ -35,7 +35,7 @@ function createBaseClient(options) {
|
|
|
35
35
|
const client = createOpenApiClient({
|
|
36
36
|
baseUrl: `${origin}/v1`,
|
|
37
37
|
fetch: options.maxRetries === 0
|
|
38
|
-
? send
|
|
38
|
+
? withoutMovingHeader(send)
|
|
39
39
|
: withRetries(send, { maxRetries: options.maxRetries }),
|
|
40
40
|
});
|
|
41
41
|
client.use({
|
package/dist/product/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PRODUCT-OWNED: Limitry's SDK helpers (fork-owned seam — docs/conventions.md
|
|
3
3
|
* "Fork-owned seams"): re-exported by the SDK entry and published with
|
|
4
|
-
* it. `guard` / `
|
|
4
|
+
* it. `guard` / `reserve` (docs/product/roadmap.md slice 6) sit on the client
|
|
5
5
|
* (`limitry.guard(…)`, the SDK's `productMethods` seam); `createLimits(client,
|
|
6
6
|
* options)` gives them with your own timeout or fail mode.
|
|
7
7
|
*/
|
|
8
8
|
import type { BaseClient } from "../index.ts";
|
|
9
|
-
export { createLimits,
|
|
10
|
-
/** Limitry's methods on the client: `limitry.guard(…)`, `limitry.
|
|
9
|
+
export { createLimits, LimitReached, Unavailable, type GuardRequest, type Limits, type LimitsOptions, type Reservation, type ReserveRequest, } from "./limits.ts";
|
|
10
|
+
/** Limitry's methods on the client: `limitry.guard(…)`, `limitry.reserve(…)`, `limitry.standing(…)`. */
|
|
11
11
|
export declare const productMethods: (client: BaseClient) => {
|
|
12
12
|
guard: <T>(request: import("./limits.ts").GuardRequest, fn: () => Promise<T>) => Promise<T>;
|
|
13
|
-
|
|
13
|
+
reserve: <T>(request: import("./limits.ts").ReserveRequest, fn: (reservation: import("./limits.ts").Reservation) => Promise<T>) => Promise<T>;
|
|
14
14
|
standing: (request: {
|
|
15
15
|
subject: {
|
|
16
16
|
kind: string;
|
package/dist/product/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createLimits } from "./limits.js";
|
|
2
|
-
export { createLimits,
|
|
3
|
-
/** Limitry's methods on the client: `limitry.guard(…)`, `limitry.
|
|
2
|
+
export { createLimits, LimitReached, Unavailable, } from "./limits.js";
|
|
3
|
+
/** Limitry's methods on the client: `limitry.guard(…)`, `limitry.reserve(…)`, `limitry.standing(…)`. */
|
|
4
4
|
export const productMethods = (client) => {
|
|
5
5
|
const limits = createLimits(client);
|
|
6
|
-
return {
|
|
6
|
+
return {
|
|
7
|
+
guard: limits.guard,
|
|
8
|
+
reserve: limits.reserve,
|
|
9
|
+
standing: limits.standing,
|
|
10
|
+
};
|
|
7
11
|
};
|
package/dist/product/limits.d.ts
CHANGED
|
@@ -13,33 +13,45 @@ import type { BaseClient, Schemas } from "../index.ts";
|
|
|
13
13
|
* );
|
|
14
14
|
*
|
|
15
15
|
* // Cost known afterwards (an AI reply's cents), or "at most N at
|
|
16
|
-
* // once":
|
|
17
|
-
* // released on failure.
|
|
18
|
-
* const answer = await limitry.
|
|
19
|
-
* { subject: { kind: "user", id: user.id }, action: "ai-call",
|
|
20
|
-
* async (
|
|
16
|
+
* // once": reserve what it will probably cost, do the work, commit
|
|
17
|
+
* // what it used — released on failure.
|
|
18
|
+
* const answer = await limitry.reserve(
|
|
19
|
+
* { subject: { kind: "user", id: user.id }, action: "ai-call", cost: 5 },
|
|
20
|
+
* async (reservation) => {
|
|
21
21
|
* const res = await callModel(prompt);
|
|
22
|
-
*
|
|
22
|
+
* reservation.cost(priceCents(res.usage));
|
|
23
23
|
* return res;
|
|
24
24
|
* },
|
|
25
25
|
* );
|
|
26
26
|
*
|
|
27
|
-
* A refusal throws `
|
|
28
|
-
* Limitry cannot be reached
|
|
29
|
-
* `timeoutMs
|
|
30
|
-
* `plan_limit_reached`)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `open` — the default, and the answer before any
|
|
34
|
-
* lets the work run unmetered.
|
|
27
|
+
* A refusal throws `LimitReached` (with `limitId` and
|
|
28
|
+
* `retryAfterSeconds`). When Limitry cannot be reached — an error, a 5xx,
|
|
29
|
+
* no answer within `timeoutMs`, the workspace past its plan's allowance
|
|
30
|
+
* (402 `plan_limit_reached`) or the edge's flood guard (429) — each
|
|
31
|
+
* limit's fail mode decides: the last decision's `failMode` for this
|
|
32
|
+
* subject and action is remembered in memory, and `closed` throws
|
|
33
|
+
* `Unavailable` while `open` — the default, and the answer before any
|
|
34
|
+
* decision was seen — lets the work run unmetered.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
36
|
+
* A subject being handed over between Limitry's servers (a split, a few
|
|
37
|
+
* seconds) answers `503 moving`: Limitry was reached and charged nothing,
|
|
38
|
+
* so it is not "unreachable" — the check (or commit, or release) is sent
|
|
39
|
+
* again with the same key for up to 30 s, each attempt with its own
|
|
40
|
+
* `timeoutMs`, before the fail mode applies. A fail-open product never
|
|
41
|
+
* runs uncounted through a handover. A limiter with too many checks
|
|
42
|
+
* inside at once answers `503 busy` the same way (charged nothing; retry
|
|
43
|
+
* with the same key): both share those 30 s, and each wait is the
|
|
44
|
+
* answer's Retry-After plus full jitter on a growing backoff, so clients
|
|
45
|
+
* turned away together don't all come back together.
|
|
46
|
+
*
|
|
47
|
+
* Retries are safe (docs/product idempotency): each guard or reserve
|
|
48
|
+
* sends one Idempotency-Key, which the client's own retries reuse, so a
|
|
49
|
+
* check counts once however often it is sent. A reservation whose check
|
|
50
|
+
* timed out may have been made all the same: once the work is done,
|
|
51
|
+
* `reserve` sends the check again with the same key — a replay, nothing
|
|
52
|
+
* counted twice — and releases the reservation it finds, rather than
|
|
53
|
+
* leave it taken until it expires. Commits and releases retry too
|
|
54
|
+
* (committing or releasing twice answers the same).
|
|
43
55
|
*/
|
|
44
56
|
type Decision = Schemas["CheckDecision"];
|
|
45
57
|
type Subject = {
|
|
@@ -60,40 +72,55 @@ export type GuardRequest = {
|
|
|
60
72
|
/** Pools it also draws from (a team's, an org's). */
|
|
61
73
|
shared?: Subject[];
|
|
62
74
|
};
|
|
63
|
-
export type
|
|
75
|
+
export type ReserveRequest = {
|
|
64
76
|
subject: Subject;
|
|
65
77
|
action: string;
|
|
66
|
-
/** What it will probably use —
|
|
67
|
-
|
|
78
|
+
/** What it will probably use — reserved until you commit the actual cost. Default 1. */
|
|
79
|
+
cost?: number;
|
|
68
80
|
shared?: Subject[];
|
|
69
|
-
/** How long the
|
|
81
|
+
/** How long the reservation lasts if never committed or released (default 300, at most 3600). */
|
|
70
82
|
ttlSeconds?: number;
|
|
71
83
|
};
|
|
72
|
-
export type
|
|
84
|
+
export type Reservation = {
|
|
73
85
|
/**
|
|
74
86
|
* The cost actually used — committed when the work returns. Default: the
|
|
75
|
-
*
|
|
76
|
-
* anything else throws a RangeError here, in your code, rather
|
|
77
|
-
* commit the API refuses after the work (whose usage would go
|
|
87
|
+
* reserved cost. A whole number from 0 to 1,000,000,000, as the API
|
|
88
|
+
* commits: anything else throws a RangeError here, in your code, rather
|
|
89
|
+
* than a commit the API refuses after the work (whose usage would go
|
|
90
|
+
* uncounted).
|
|
78
91
|
*/
|
|
79
92
|
cost(units: number): void;
|
|
80
93
|
/**
|
|
81
|
-
* Keep the
|
|
82
|
-
* (default 300, 1 to 3600). Throws the API's error, such as for a
|
|
83
|
-
* that already expired.
|
|
94
|
+
* Keep the reservation for longer work: it expires `ttlSeconds` from now
|
|
95
|
+
* (default 300, 1 to 3600). Throws the API's error, such as for a
|
|
96
|
+
* reservation that already expired.
|
|
84
97
|
*/
|
|
85
98
|
extend(ttlSeconds?: number): Promise<void>;
|
|
86
99
|
};
|
|
87
|
-
/**
|
|
88
|
-
export declare class
|
|
89
|
-
readonly reason: "
|
|
90
|
-
/** The limit that refused; null
|
|
91
|
-
readonly
|
|
100
|
+
/** Refused: a limit is reached, or the subject holds too many open reservations. */
|
|
101
|
+
export declare class LimitReached extends Error {
|
|
102
|
+
readonly reason: "limit_reached" | "too_many_reservations";
|
|
103
|
+
/** The limit that refused; null for too many open reservations. */
|
|
104
|
+
readonly limitId: string | null;
|
|
92
105
|
readonly retryAfterSeconds: number | null;
|
|
93
|
-
readonly decision: Decision
|
|
94
|
-
constructor(decision: Decision
|
|
106
|
+
readonly decision: Decision;
|
|
107
|
+
constructor(decision: Decision);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Limitry could not be reached (an error, a 5xx, a timeout, the plan's
|
|
111
|
+
* allowance used up, or the flood guard's 429) and a limit on this
|
|
112
|
+
* subject and action fails closed. With `open`, the work runs instead.
|
|
113
|
+
*/
|
|
114
|
+
export declare class Unavailable extends Error {
|
|
115
|
+
constructor();
|
|
95
116
|
}
|
|
96
|
-
export
|
|
117
|
+
export type LimitsInternals = {
|
|
118
|
+
/** Waits (tests pass a fake). */
|
|
119
|
+
sleep?: (ms: number) => Promise<void>;
|
|
120
|
+
/** 0 ≤ x < 1 for the jitter (tests pass a fixed one). */
|
|
121
|
+
random?: () => number;
|
|
122
|
+
};
|
|
123
|
+
export declare function createLimits(client: BaseClient, options?: LimitsOptions, internals?: LimitsInternals): {
|
|
97
124
|
/**
|
|
98
125
|
* Where a subject stands on every limit on it (and on `shared`
|
|
99
126
|
* subjects): used, remaining, resetsAt — for showing an end user. Call
|
|
@@ -108,12 +135,12 @@ export declare function createLimits(client: BaseClient, options?: LimitsOptions
|
|
|
108
135
|
/** Checks `cost`, runs `fn` only if allowed, and returns its result. */
|
|
109
136
|
guard<T>(request: GuardRequest, fn: () => Promise<T>): Promise<T>;
|
|
110
137
|
/**
|
|
111
|
-
*
|
|
112
|
-
* (`
|
|
113
|
-
* `fn` throws, and rethrows. A concurrent limit's slot
|
|
114
|
-
* exactly as long as `fn` runs.
|
|
138
|
+
* Reserves `cost`, runs `fn`, then commits the cost it reported
|
|
139
|
+
* (`reservation.cost(n)`, default the reserved cost) — or releases the
|
|
140
|
+
* reservation if `fn` throws, and rethrows. A concurrent limit's slot
|
|
141
|
+
* is taken for exactly as long as `fn` runs.
|
|
115
142
|
*/
|
|
116
|
-
|
|
143
|
+
reserve<T>(request: ReserveRequest, fn: (reservation: Reservation) => Promise<T>): Promise<T>;
|
|
117
144
|
};
|
|
118
145
|
export type Limits = ReturnType<typeof createLimits>;
|
|
119
146
|
export {};
|