@listen-fire/anthropic 0.12.0-rc.7 → 0.12.0-rc.8

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/dist/client.js CHANGED
@@ -10,7 +10,6 @@ const node_async_hooks_1 = require("node:async_hooks");
10
10
  const node_crypto_1 = require("node:crypto");
11
11
  const exponential_backoff_1 = require("exponential-backoff");
12
12
  const behaviour_json_1 = __importDefault(require("./behaviour.json"));
13
- const retry_after_1 = require("./retry_after");
14
13
  const structured_1 = require("./structured");
15
14
  const web_tools_1 = require("./web_tools");
16
15
  const limiter_1 = require("@listen-fire/engine/public/limiter");
@@ -18,47 +17,63 @@ const log_1 = require("@listen-fire/engine/public/log");
18
17
  const models_1 = require("@listen-fire/engine/public/models");
19
18
  const types_1 = require("@listen-fire/engine/public/types");
20
19
  const SECOND = 1000;
20
+ /** Who every model request is made as: where the vendor's rate limit lands
21
+ * (its throttle key) and what `@lf/anthropic`'s declared limit counts. */
22
+ const MODELS_CALLER = { module: '@lf/anthropic', team: '' };
21
23
  /** Requests in flight at once: `@lf/anthropic`'s own declared limit, kept by
22
24
  * the limiter slot (services/limiter), as lf's Rust client keeps it. A
23
- * request holds its place through its retries. */
24
- const MODELS_REQUEST = (0, limiter_1.limitRequest)({ module: '@lf/anthropic', team: '' }, [
25
+ * request holds its place through its retries, but not through a vendor's
26
+ * rate limit: that is a throttle on the caller, waited out in the queue. */
27
+ const MODELS_REQUEST = (0, limiter_1.limitRequest)(MODELS_CALLER, [
25
28
  { scope: 'module', concurrent: behaviour_json_1.default.concurrentRequests },
26
29
  ]);
27
30
  // behaviour.json, which the Rust lf's client is held to as well.
28
31
  const RETRY_LIMIT = behaviour_json_1.default.retry.attempts;
29
32
  const INITIAL_DELAY = behaviour_json_1.default.retry.initialDelayMs;
30
33
  const TIME_MULTIPLE = behaviour_json_1.default.retry.multiple;
31
- const RATE_LIMIT_DELAY = behaviour_json_1.default.retry.rateLimitWaitMs;
34
+ const RATE_LIMIT_WAIT = behaviour_json_1.default.retry.rateLimitWaitMs;
35
+ function rateLimited(e) {
36
+ return e instanceof Error && e.message.includes('429');
37
+ }
32
38
  async function enqueueQuery(fn, signal) {
33
39
  // Every request — each turn of a tool loop, each continuation — first asks
34
40
  // whether its run has spent its cost cap. Here, in the caller's own async
35
41
  // context: the queued job may be started from another job's.
36
42
  (0, models_1.assertRunBudget)();
37
- return (0, limiter_1.limited)((0, limiter_1.limiterSlot)(), MODELS_REQUEST, async () => {
38
- return (0, exponential_backoff_1.backOff)(fn, {
39
- jitter: 'none',
40
- numOfAttempts: RETRY_LIMIT,
41
- startingDelay: INITIAL_DELAY,
42
- timeMultiple: TIME_MULTIPLE,
43
- retry: async (e, attempt) => {
44
- if (signal?.aborted)
45
- return false;
46
- if (e instanceof Error && e.message.includes('429')) {
47
- const asked = (0, retry_after_1.rateLimitDelayMs)(e);
48
- const wait = asked ?? RATE_LIMIT_DELAY;
49
- log_1.logger.info(`Hit model vendor rate limit, waiting ${wait / SECOND}s (${asked === undefined ? 'default' : 'from retry-after'})`);
50
- await new Promise((resolve) => setTimeout(resolve, wait));
51
- }
52
- else if (e instanceof Error && e.message.includes('401')) {
53
- return false;
54
- }
55
- if (attempt < RETRY_LIMIT) {
56
- log_1.logger.info(`Retrying Anthropic query in ${(INITIAL_DELAY / SECOND) * TIME_MULTIPLE ** (attempt - 1)}s`, { error: e });
57
- }
58
- return true;
59
- },
60
- });
61
- });
43
+ for (let rateLimits = 1;; rateLimits++) {
44
+ try {
45
+ return await (0, limiter_1.limited)((0, limiter_1.limiterSlot)(), MODELS_REQUEST, async () => {
46
+ return (0, exponential_backoff_1.backOff)(fn, {
47
+ jitter: 'none',
48
+ numOfAttempts: RETRY_LIMIT,
49
+ startingDelay: INITIAL_DELAY,
50
+ timeMultiple: TIME_MULTIPLE,
51
+ retry: async (e, attempt) => {
52
+ if (signal?.aborted)
53
+ return false;
54
+ // Out of the permit: the throttle below holds every request
55
+ // made as this caller, not just this one.
56
+ if (rateLimited(e))
57
+ return false;
58
+ if (e instanceof Error && e.message.includes('401'))
59
+ return false;
60
+ if (attempt < RETRY_LIMIT) {
61
+ log_1.logger.info(`Retrying Anthropic query in ${(INITIAL_DELAY / SECOND) * TIME_MULTIPLE ** (attempt - 1)}s`, { error: e });
62
+ }
63
+ return true;
64
+ },
65
+ });
66
+ });
67
+ }
68
+ catch (e) {
69
+ if (!rateLimited(e) || signal?.aborted || rateLimits >= RETRY_LIMIT)
70
+ throw e;
71
+ const asked = (0, limiter_1.vendorWaitMs)(e);
72
+ const wait = asked ?? RATE_LIMIT_WAIT;
73
+ log_1.logger.info(`Hit model vendor rate limit, throttling model requests for ${wait / SECOND}s (${asked === undefined ? 'default' : 'from retry-after'})`);
74
+ await (0, limiter_1.throttleAfter)((0, limiter_1.limiterSlot)(), MODELS_CALLER, wait);
75
+ }
76
+ }
62
77
  }
63
78
  function emptyTally() {
64
79
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@listen-fire/anthropic",
3
3
  "license": "Apache-2.0",
4
- "version": "0.12.0-rc.7",
4
+ "version": "0.12.0-rc.8",
5
5
  "description": "The models slot's module for Claude (`@lf/anthropic`): the client every model call goes through (chat, structured output, the tool loop, web chat with Anthropic's hosted search and fetch), the chat seam a provider stands behind, Anthropic's API on a key the caller holds, the price table every call is billed from (prices.json, which the Rust lf embeds too), and the extraction and `AI()` clients the engine's models slot is filled with.",
6
6
  "engines": {
7
7
  "node": "22.20.0"
@@ -14,7 +14,7 @@
14
14
  "exponential-backoff": "3.1.1",
15
15
  "jsonrepair": "3.8.0",
16
16
  "zod": "4.1.12",
17
- "@listen-fire/engine": "^0.12.0-rc.7"
17
+ "@listen-fire/engine": "^0.12.0-rc.8"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/jest": "29.5.14",
@@ -1,6 +0,0 @@
1
- /**
2
- * How long the vendor asked a rate-limited caller to wait, or undefined when
3
- * it did not say. Clamped to [1s, 120s] so a hostile or absurd value can
4
- * neither spin nor stall a queue slot.
5
- */
6
- export declare function rateLimitDelayMs(e: unknown, now?: number): number | undefined;
@@ -1,57 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.rateLimitDelayMs = rateLimitDelayMs;
4
- const SECOND = 1000;
5
- const MIN_DELAY = 1 * SECOND;
6
- const MAX_DELAY = 120 * SECOND;
7
- function clamp(ms) {
8
- return Math.min(MAX_DELAY, Math.max(MIN_DELAY, Math.round(ms)));
9
- }
10
- /** `Retry-After` is either whole seconds or an HTTP date. */
11
- function fromHeaderValue(value, now) {
12
- const trimmed = value.trim();
13
- if (/^\d+(\.\d+)?$/.test(trimmed))
14
- return Number(trimmed) * SECOND;
15
- const date = Date.parse(trimmed);
16
- return Number.isNaN(date) ? undefined : date - now;
17
- }
18
- function readHeader(headers, name) {
19
- if (!headers || typeof headers !== 'object')
20
- return undefined;
21
- if ('get' in headers && typeof headers.get === 'function') {
22
- const value = headers.get(name);
23
- return typeof value === 'string' ? value : undefined;
24
- }
25
- for (const [key, value] of Object.entries(headers)) {
26
- if (key.toLowerCase() === name && typeof value === 'string')
27
- return value;
28
- }
29
- return undefined;
30
- }
31
- /**
32
- * Google's `google.rpc.RetryInfo` detail: `"retryDelay": "17s"` (or
33
- * `"17.5s"`) on the wire, `{ seconds: 17 }` in a decoded form. The genai SDK's
34
- * `ApiError` carries the JSON body only as text in its message.
35
- */
36
- function fromRetryInfo(text) {
37
- const quoted = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(text);
38
- if (quoted)
39
- return Number(quoted[1]) * SECOND;
40
- const object = /"retryDelay"\s*:\s*\{\s*"seconds"\s*:\s*"?(\d+)"?/.exec(text);
41
- return object ? Number(object[1]) * SECOND : undefined;
42
- }
43
- /**
44
- * How long the vendor asked a rate-limited caller to wait, or undefined when
45
- * it did not say. Clamped to [1s, 120s] so a hostile or absurd value can
46
- * neither spin nor stall a queue slot.
47
- */
48
- function rateLimitDelayMs(e, now = Date.now()) {
49
- const headers = typeof e === 'object' && e !== null && 'headers' in e ? e.headers : undefined;
50
- const header = readHeader(headers, 'retry-after');
51
- const fromHeader = header === undefined ? undefined : fromHeaderValue(header, now);
52
- if (fromHeader !== undefined)
53
- return clamp(fromHeader);
54
- const text = e instanceof Error ? e.message : undefined;
55
- const fromBody = text === undefined ? undefined : fromRetryInfo(text);
56
- return fromBody === undefined ? undefined : clamp(fromBody);
57
- }