@listen-fire/anthropic 0.12.0-rc.8 → 0.12.0
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/chat.d.ts +4 -0
- package/dist/client.js +19 -16
- package/dist/keyed_provider.d.ts +8 -0
- package/dist/keyed_provider.js +33 -1
- package/package.json +4 -5
package/dist/chat.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import type { ChatModelName, Resolved } from '@listen-fire/engine/public/models';
|
|
3
3
|
export type ChatProvider = {
|
|
4
|
+
/** A stable, non-reversible id of the API key this provider calls on, when
|
|
5
|
+
* it has one: Anthropic's limits are per key, so the limiter counts and
|
|
6
|
+
* throttles each key apart. A provider with none shares one module-wide. */
|
|
7
|
+
readonly credential?: string;
|
|
4
8
|
messages: {
|
|
5
9
|
create(params: Anthropic.MessageCreateParamsNonStreaming, options?: Anthropic.RequestOptions): Promise<Anthropic.Message>;
|
|
6
10
|
stream(params: Anthropic.MessageCreateParamsNonStreaming, options?: Anthropic.RequestOptions): {
|
package/dist/client.js
CHANGED
|
@@ -12,21 +12,22 @@ const exponential_backoff_1 = require("exponential-backoff");
|
|
|
12
12
|
const behaviour_json_1 = __importDefault(require("./behaviour.json"));
|
|
13
13
|
const structured_1 = require("./structured");
|
|
14
14
|
const web_tools_1 = require("./web_tools");
|
|
15
|
+
const keyed_provider_1 = require("./keyed_provider");
|
|
15
16
|
const limiter_1 = require("@listen-fire/engine/public/limiter");
|
|
16
17
|
const log_1 = require("@listen-fire/engine/public/log");
|
|
17
18
|
const models_1 = require("@listen-fire/engine/public/models");
|
|
18
19
|
const types_1 = require("@listen-fire/engine/public/types");
|
|
19
20
|
const SECOND = 1000;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Requests in flight at once: `@lf/anthropic`'s own declared limit,
|
|
24
|
-
* the limiter slot (services/limiter), as lf's Rust client keeps it.
|
|
25
|
-
* request holds its place through its retries, but not through a vendor's
|
|
21
|
+
function modelsCaller(provider) {
|
|
22
|
+
return (0, keyed_provider_1.modelsCallerFor)(provider.credential);
|
|
23
|
+
}
|
|
24
|
+
/** Requests in flight at once, per key: `@lf/anthropic`'s own declared limit,
|
|
25
|
+
* kept by the limiter slot (services/limiter), as lf's Rust client keeps it.
|
|
26
|
+
* A request holds its place through its retries, but not through a vendor's
|
|
26
27
|
* rate limit: that is a throttle on the caller, waited out in the queue. */
|
|
27
|
-
|
|
28
|
-
{ scope: '
|
|
29
|
-
|
|
28
|
+
function modelsRequest(caller) {
|
|
29
|
+
return (0, limiter_1.limitRequest)(caller, [{ scope: 'credential', concurrent: behaviour_json_1.default.concurrentRequests }]);
|
|
30
|
+
}
|
|
30
31
|
// behaviour.json, which the Rust lf's client is held to as well.
|
|
31
32
|
const RETRY_LIMIT = behaviour_json_1.default.retry.attempts;
|
|
32
33
|
const INITIAL_DELAY = behaviour_json_1.default.retry.initialDelayMs;
|
|
@@ -35,14 +36,16 @@ const RATE_LIMIT_WAIT = behaviour_json_1.default.retry.rateLimitWaitMs;
|
|
|
35
36
|
function rateLimited(e) {
|
|
36
37
|
return e instanceof Error && e.message.includes('429');
|
|
37
38
|
}
|
|
38
|
-
async function enqueueQuery(fn, signal) {
|
|
39
|
+
async function enqueueQuery(provider, fn, signal) {
|
|
40
|
+
const caller = modelsCaller(provider);
|
|
41
|
+
const request = modelsRequest(caller);
|
|
39
42
|
// Every request — each turn of a tool loop, each continuation — first asks
|
|
40
43
|
// whether its run has spent its cost cap. Here, in the caller's own async
|
|
41
44
|
// context: the queued job may be started from another job's.
|
|
42
45
|
(0, models_1.assertRunBudget)();
|
|
43
46
|
for (let rateLimits = 1;; rateLimits++) {
|
|
44
47
|
try {
|
|
45
|
-
return await (0, limiter_1.limited)((0, limiter_1.limiterSlot)(),
|
|
48
|
+
return await (0, limiter_1.limited)((0, limiter_1.limiterSlot)(), request, async () => {
|
|
46
49
|
return (0, exponential_backoff_1.backOff)(fn, {
|
|
47
50
|
jitter: 'none',
|
|
48
51
|
numOfAttempts: RETRY_LIMIT,
|
|
@@ -71,7 +74,7 @@ async function enqueueQuery(fn, signal) {
|
|
|
71
74
|
const asked = (0, limiter_1.vendorWaitMs)(e);
|
|
72
75
|
const wait = asked ?? RATE_LIMIT_WAIT;
|
|
73
76
|
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)(),
|
|
77
|
+
await (0, limiter_1.throttleAfter)((0, limiter_1.limiterSlot)(), caller, wait);
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -256,7 +259,7 @@ async function anthropicToolLoop(access, params, toolImpls = {}) {
|
|
|
256
259
|
throw new Error(`Anthropic tool loop exhausted its turn budget (maxTurns=${maxTurns})${label ? ` in ${label}` : ''} — the agent never finished.`);
|
|
257
260
|
}
|
|
258
261
|
const llmStart = Date.now();
|
|
259
|
-
const response = await enqueueQuery(async () => {
|
|
262
|
+
const response = await enqueueQuery(client, async () => {
|
|
260
263
|
return client.messages.create({
|
|
261
264
|
model: wireModel,
|
|
262
265
|
max_tokens: max_output_tokens,
|
|
@@ -578,7 +581,7 @@ async function anthropicChatDetailed(access, options) {
|
|
|
578
581
|
// Streamed rather than awaited whole: above ~16K `max_tokens` a
|
|
579
582
|
// non-streaming request outlives the SDK's HTTP timeout. `finalMessage()`
|
|
580
583
|
// reassembles the same `Message`, so nothing downstream changes.
|
|
581
|
-
response = await enqueueQuery(async () => {
|
|
584
|
+
response = await enqueueQuery(client, async () => {
|
|
582
585
|
const stream = client.messages.stream({
|
|
583
586
|
model: wireModel,
|
|
584
587
|
max_tokens: maxTokens,
|
|
@@ -719,7 +722,7 @@ async function anthropicChatStructured(access, options) {
|
|
|
719
722
|
const { client, wireModel, resolved } = access.chatCallFor(model);
|
|
720
723
|
const tool = (0, structured_1.buildStructuredTool)({ name: toolName, description: toolDescription, schema });
|
|
721
724
|
const startMs = Date.now();
|
|
722
|
-
const response = await enqueueQuery(async () => client.messages.create({
|
|
725
|
+
const response = await enqueueQuery(client, async () => client.messages.create({
|
|
723
726
|
model: wireModel,
|
|
724
727
|
max_tokens: maxTokens,
|
|
725
728
|
system: [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }],
|
|
@@ -947,7 +950,7 @@ async function anthropicWebChat(access, options) {
|
|
|
947
950
|
const settleWatch = watchSlowCall({ ...callFields, turn });
|
|
948
951
|
let response;
|
|
949
952
|
try {
|
|
950
|
-
response = await enqueueQuery(async () => {
|
|
953
|
+
response = await enqueueQuery(client, async () => {
|
|
951
954
|
const stream = client.messages.stream({
|
|
952
955
|
model: wireModel,
|
|
953
956
|
max_tokens: maxTokens,
|
package/dist/keyed_provider.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { type Caller } from '@listen-fire/engine/public/limiter';
|
|
1
2
|
import type { ChatProvider } from './chat';
|
|
2
3
|
export interface AnthropicKey {
|
|
3
4
|
apiKey: string;
|
|
4
5
|
/** Another endpoint speaking the same Messages API (a proxy, a test fake). */
|
|
5
6
|
baseUrl?: string;
|
|
6
7
|
}
|
|
8
|
+
/** What the limiter knows a key by: a hash, so neither the key nor any prefix
|
|
9
|
+
* of it reaches a throttle key or a shared limiter's store. */
|
|
10
|
+
export declare function credentialId(apiKey: string): string;
|
|
11
|
+
/** Who a model request is made as: the API key it calls on (Anthropic's
|
|
12
|
+
* limits are per key), so a 429 on one key leaves the others open. A provider
|
|
13
|
+
* with no key of its own is one caller, module-wide. */
|
|
14
|
+
export declare function modelsCallerFor(credential: string | undefined): Caller;
|
|
7
15
|
export declare function anthropicKeyedChatProvider({ apiKey, baseUrl }: AnthropicKey): ChatProvider;
|
package/dist/keyed_provider.js
CHANGED
|
@@ -5,16 +5,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.credentialId = credentialId;
|
|
9
|
+
exports.modelsCallerFor = modelsCallerFor;
|
|
8
10
|
exports.anthropicKeyedChatProvider = anthropicKeyedChatProvider;
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
12
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
13
|
+
const limiter_1 = require("@listen-fire/engine/public/limiter");
|
|
10
14
|
// One per key rather than one per process: a unit that pays with its own key
|
|
11
15
|
// (knowledge's `KNOWLEDGE_LLM_API_KEY`) must not share the platform's client.
|
|
12
16
|
const keyedClients = new Map();
|
|
17
|
+
/** What the limiter knows a key by: a hash, so neither the key nor any prefix
|
|
18
|
+
* of it reaches a throttle key or a shared limiter's store. */
|
|
19
|
+
function credentialId(apiKey) {
|
|
20
|
+
return `sha256:${(0, node_crypto_1.createHash)('sha256').update(apiKey).digest('hex').slice(0, 16)}`;
|
|
21
|
+
}
|
|
22
|
+
/** Who a model request is made as: the API key it calls on (Anthropic's
|
|
23
|
+
* limits are per key), so a 429 on one key leaves the others open. A provider
|
|
24
|
+
* with no key of its own is one caller, module-wide. */
|
|
25
|
+
function modelsCallerFor(credential) {
|
|
26
|
+
return { module: '@lf/anthropic', team: '', ...(credential !== undefined ? { credential } : {}) };
|
|
27
|
+
}
|
|
28
|
+
/** Every response the key's client receives reports the key's remaining
|
|
29
|
+
* budget; a spent one closes the key before the 429 would. A failed
|
|
30
|
+
* response is the handler's (it reads the 429's own Retry-After). */
|
|
31
|
+
function fetchReportingBudget(credential) {
|
|
32
|
+
return async (input, init) => {
|
|
33
|
+
const response = await fetch(input, init);
|
|
34
|
+
if (response.ok)
|
|
35
|
+
await (0, limiter_1.budgetAfter)((0, limiter_1.limiterSlot)(), modelsCallerFor(credential), (0, limiter_1.vendorBudget)(response.headers));
|
|
36
|
+
return response;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
13
39
|
function anthropicKeyedChatProvider({ apiKey, baseUrl }) {
|
|
14
40
|
const cacheKey = `${baseUrl ?? ''}\n${apiKey}`;
|
|
15
41
|
let keyed = keyedClients.get(cacheKey);
|
|
16
42
|
if (!keyed) {
|
|
17
|
-
|
|
43
|
+
// No SDK retries: a 429 must reach the module's handler at once, which
|
|
44
|
+
// releases the permit and throttles the key for everyone sharing the
|
|
45
|
+
// limiter. The SDK would sleep on it (twice) inside the held permit.
|
|
46
|
+
// The client's own backoff retries the other transient failures.
|
|
47
|
+
const credential = credentialId(apiKey);
|
|
48
|
+
const sdk = new sdk_1.default({ apiKey, maxRetries: 0, fetch: fetchReportingBudget(credential), ...(baseUrl !== undefined ? { baseURL: baseUrl } : {}) });
|
|
49
|
+
keyed = { credential, messages: sdk.messages };
|
|
18
50
|
keyedClients.set(cacheKey, keyed);
|
|
19
51
|
}
|
|
20
52
|
return keyed;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@listen-fire/anthropic",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.12.0
|
|
4
|
+
"version": "0.12.0",
|
|
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
|
-
"node": "22.
|
|
7
|
+
"node": ">=22.23.4 <23"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
@@ -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
|
|
17
|
+
"@listen-fire/engine": "^0.12.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/jest": "29.5.14",
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
32
|
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
34
|
-
"tag": "next"
|
|
33
|
+
"access": "public"
|
|
35
34
|
},
|
|
36
35
|
"scripts": {
|
|
37
36
|
"code:type-check": "tsc --pretty --noEmit && node ../../scripts/check-module-boundary.cjs",
|