@listen-fire/anthropic 0.12.0-rc.6 → 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 +44 -29
- package/package.json +2 -2
- package/dist/retry_after.d.ts +0 -6
- package/dist/retry_after.js +0 -57
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
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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.
|
|
17
|
+
"@listen-fire/engine": "^0.12.0-rc.8"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/jest": "29.5.14",
|
package/dist/retry_after.d.ts
DELETED
|
@@ -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;
|
package/dist/retry_after.js
DELETED
|
@@ -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
|
-
}
|