@limitry/sdk 1.0.0-rc.6 → 1.0.0-rc.7

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 CHANGED
@@ -2,6 +2,32 @@
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.7 (2026-10-19)
6
+
7
+ ### Added
8
+
9
+ - A refused check's `reason` has a `message`: the refusal in one sentence,
10
+ such as "Refused by AI budget: 3 cents left, resets
11
+ 2026-11-01T00:00:00.000Z. Try again in 3600 s." `LimitExceeded` and
12
+ `limitry check` show it as the API writes it, so every client says the
13
+ same. Branch on `allowed` and `reason.code`, never on the message.
14
+
15
+ ### Fixed
16
+
17
+ - - `held.cost()` refuses a cost the API can't commit (not a whole number
18
+ from 0 to 1,000,000,000): TypeScript throws `RangeError`, Python raises
19
+ `ValueError`, in your code. Before, the commit failed silently after
20
+ the work and its usage was never counted.
21
+ - TypeScript's `held.extend()` throws the API's error, such as for an
22
+ expired hold, as Python's already did. Both send the `ttlSeconds` you
23
+ give as it is, so `0` is refused rather than read as the default.
24
+ - Python checks, commits and releases now retry exactly as TypeScript's
25
+ do: a network error, `408`, `429` or `5xx`, at most twice, all within
26
+ the check's `timeout` (1.5 s) or 5 s for a settle. `standing()` retries
27
+ too, and reports an error answer that isn't JSON.
28
+ - `limitry check` and `limitry sync` refuse `--workspace` with an API key,
29
+ as every other command does, instead of ignoring it.
30
+
5
31
  ## 1.0.0-rc.6 (2026-10-19)
6
32
 
7
33
  ### Changed
package/dist/index.d.ts CHANGED
@@ -79,6 +79,7 @@ declare function createBaseClient(options: ClientOptions): import("openapi-fetch
79
79
  reason: {
80
80
  code: "limit_exceeded" | "too_many_reservations";
81
81
  limit: string | null;
82
+ message: string;
82
83
  } | null;
83
84
  remaining: components["schemas"]["CheckRemaining"][];
84
85
  reservation?: {
@@ -70,9 +70,18 @@ export type HoldRequest = {
70
70
  ttlSeconds?: number;
71
71
  };
72
72
  export type Held = {
73
- /** The cost actually used — committed when the work returns. Default: the estimate. */
73
+ /**
74
+ * The cost actually used — committed when the work returns. Default: the
75
+ * estimate. A whole number from 0 to 1,000,000,000, as the API commits:
76
+ * anything else throws a RangeError here, in your code, rather than a
77
+ * commit the API refuses after the work (whose usage would go uncounted).
78
+ */
74
79
  cost(units: number): void;
75
- /** Keep the hold for longer work: it expires `ttlSeconds` from now. */
80
+ /**
81
+ * Keep the hold for longer work: it expires `ttlSeconds` from now
82
+ * (default 300, 1 to 3600). Throws the API's error, such as for a hold
83
+ * that already expired.
84
+ */
76
85
  extend(ttlSeconds?: number): Promise<void>;
77
86
  };
78
87
  /** Not allowed: a limit is used up — or Limitry is unreachable and a limit fails closed. */
@@ -1,18 +1,11 @@
1
- /**
2
- * A refusal in the CLI's words, naming the limit rather than its id:
3
- * "Refused by AI replies per day: 0 replies left, resets …. Try again in
4
- * 42 s."
5
- */
6
- function refusal(decision) {
7
- const id = decision?.reason?.limit ?? null;
8
- const limit = decision?.remaining.find((r) => r.limit === id);
9
- const retry = decision?.retryAfterSeconds != null
10
- ? ` Try again in ${decision.retryAfterSeconds} s.`
11
- : "";
12
- if (!limit)
13
- return `Refused: limit ${id ?? "(unknown)"} is used up.${retry}`;
14
- const resets = limit.resetsAt ? `, resets ${limit.resetsAt}` : "";
15
- return `Refused by ${limit.name}: ${limit.remaining} ${limit.unit} left${resets}.${retry}`;
1
+ import { apiErrorFrom } from "../errors.js";
2
+ /** The largest cost a commit takes. */
3
+ const MAX_COST = 1_000_000_000;
4
+ function checkedCost(units) {
5
+ if (!Number.isInteger(units) || units < 0 || units > MAX_COST) {
6
+ throw new RangeError(`held.cost(${units}): the cost must be a whole number from 0 to ${MAX_COST}`);
7
+ }
8
+ return units;
16
9
  }
17
10
  /** Not allowed: a limit is used up — or Limitry is unreachable and a limit fails closed. */
18
11
  export class LimitExceeded extends Error {
@@ -25,9 +18,8 @@ export class LimitExceeded extends Error {
25
18
  const reason = decision?.reason?.code ?? "unavailable";
26
19
  super(reason === "unavailable"
27
20
  ? "Limitry could not be reached and a limit denies when unreachable"
28
- : reason === "too_many_reservations"
29
- ? "Too many open reservations for this subject"
30
- : refusal(decision));
21
+ : // The API's own sentence (reason.message), one wording everywhere.
22
+ (decision?.reason?.message ?? "Refused"));
31
23
  this.name = "LimitExceeded";
32
24
  this.reason = reason;
33
25
  this.limit = decision?.reason?.limit ?? null;
@@ -201,7 +193,12 @@ export function createLimits(client, options = {}) {
201
193
  };
202
194
  const idempotencyKey = crypto.randomUUID();
203
195
  const decision = await check(body, idempotencyKey);
204
- const unmetered = { cost: () => { }, extend: async () => { } };
196
+ const unmetered = {
197
+ // Checked even unmetered: the mistake shows whether or not
198
+ // Limitry answered.
199
+ cost: (units) => void checkedCost(units),
200
+ extend: async () => { },
201
+ };
205
202
  if (decision === UNREACHABLE || decision === TIMED_OUT) {
206
203
  const unheard = () => decision === TIMED_OUT
207
204
  ? releaseUnheard(body, idempotencyKey)
@@ -225,14 +222,17 @@ export function createLimits(client, options = {}) {
225
222
  let used = estimate;
226
223
  const held = {
227
224
  cost(units) {
228
- used = units;
225
+ used = checkedCost(units);
229
226
  },
230
227
  async extend(ttlSeconds) {
231
- await client.POST("/reservations/{reservationId}/extend", {
228
+ // Passed as given: the API refuses a bad one (0, past 3600).
229
+ const { error, response } = await client.POST("/reservations/{reservationId}/extend", {
232
230
  params: { path: { reservationId: id } },
233
- body: ttlSeconds ? { ttlSeconds } : {},
231
+ body: ttlSeconds === undefined ? {} : { ttlSeconds },
234
232
  signal: AbortSignal.timeout(timeoutMs),
235
233
  });
234
+ if (error !== undefined)
235
+ throw apiErrorFrom(error, response);
236
236
  },
237
237
  };
238
238
  let result;
@@ -8,6 +8,7 @@ export declare function resources(client: RawClient): {
8
8
  reason: {
9
9
  code: "limit_exceeded" | "too_many_reservations";
10
10
  limit: string | null;
11
+ message: string;
11
12
  } | null;
12
13
  remaining: import("./v1.gen.ts").components["schemas"]["CheckRemaining"][];
13
14
  reservation?: {
package/dist/v1.gen.d.ts CHANGED
@@ -778,6 +778,11 @@ export interface components {
778
778
  code: "limit_exceeded" | "too_many_reservations";
779
779
  /** @example lim_3f0c… */
780
780
  limit: string | null;
781
+ /**
782
+ * @description The refusal in one sentence, for people and logs: show it, never parse it (its wording may change). Branch on `allowed` and `code`.
783
+ * @example Refused by AI replies per day: 0 replies left, resets 2026-11-01T00:00:00.000Z. Try again in 3600 s.
784
+ */
785
+ message: string;
781
786
  } | null;
782
787
  /** @description Every limit that applied, and what is left in its window. */
783
788
  remaining: components["schemas"]["CheckRemaining"][];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limitry/sdk",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "Limitry API client",
5
5
  "type": "module",
6
6
  "exports": {