nexus-ai-pro 2.0.0 → 2.2.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/API_STABILITY.md +65 -0
- package/CHANGELOG.md +267 -1
- package/MIGRATING.md +1 -1
- package/README.md +17 -8
- package/dist/agent/create-agent.d.ts +9 -1
- package/dist/agent/create-agent.js +40 -1
- package/dist/cache/memory-cache.d.ts +8 -0
- package/dist/cache/memory-cache.js +9 -0
- package/dist/cache/semantic-cache.d.ts +6 -3
- package/dist/cache/semantic-cache.js +5 -2
- package/dist/cli/args.js +3 -0
- package/dist/cli/db.d.ts +16 -4
- package/dist/cli/db.js +169 -22
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +53 -0
- package/dist/cli/graph.d.ts +9 -0
- package/dist/cli/graph.js +47 -0
- package/dist/cli.js +21 -2
- package/dist/core/lifecycle.js +11 -6
- package/dist/core/nexus.d.ts +12 -2
- package/dist/core/nexus.js +106 -23
- package/dist/doctor/index.d.ts +90 -0
- package/dist/doctor/index.js +237 -0
- package/dist/evals/judge.d.ts +11 -0
- package/dist/evals/judge.js +33 -0
- package/dist/evaluate/index.d.ts +1 -0
- package/dist/evaluate/index.js +1 -0
- package/dist/evaluate/provenance.d.ts +22 -0
- package/dist/evaluate/provenance.js +10 -0
- package/dist/evaluate/run.js +20 -4
- package/dist/graph/durability.d.ts +27 -0
- package/dist/graph/durability.js +66 -0
- package/dist/graph/errors.d.ts +30 -4
- package/dist/graph/errors.js +18 -2
- package/dist/graph/event-stream.d.ts +75 -0
- package/dist/graph/event-stream.js +289 -0
- package/dist/graph/functional.d.ts +80 -7
- package/dist/graph/functional.js +138 -35
- package/dist/graph/graph.d.ts +56 -8
- package/dist/graph/graph.js +331 -49
- package/dist/graph/index.d.ts +4 -2
- package/dist/graph/index.js +3 -1
- package/dist/graph/lint.d.ts +57 -0
- package/dist/graph/lint.js +141 -0
- package/dist/graph/run-control.d.ts +28 -0
- package/dist/graph/run-control.js +26 -0
- package/dist/images/evals.js +4 -2
- package/dist/operations/adapters.d.ts +52 -4
- package/dist/operations/adapters.js +194 -7
- package/dist/operations/runner.js +14 -15
- package/dist/operations/stats.js +8 -1
- package/dist/operations/store.d.ts +5 -1
- package/dist/operations/store.js +7 -0
- package/dist/ops/circuit-breaker.d.ts +52 -0
- package/dist/ops/circuit-breaker.js +4 -0
- package/dist/ops/circuit-store.d.ts +13 -2
- package/dist/ops/circuit-store.js +68 -0
- package/dist/ops/circuit-sync.d.ts +17 -0
- package/dist/ops/circuit-sync.js +86 -0
- package/dist/ops/health.d.ts +24 -0
- package/dist/ops/health.js +38 -10
- package/dist/ops/index.d.ts +1 -1
- package/dist/ops/index.js +1 -1
- package/dist/ops/rate-limit-adapters.d.ts +23 -0
- package/dist/ops/rate-limit-adapters.js +34 -0
- package/dist/ops/rate-limiter.d.ts +14 -2
- package/dist/ops/rate-limiter.js +31 -0
- package/dist/postgres/circuits.d.ts +21 -4
- package/dist/postgres/circuits.js +65 -4
- package/dist/postgres/client.d.ts +0 -2
- package/dist/postgres/client.js +0 -4
- package/dist/postgres/evaluate.d.ts +16 -4
- package/dist/postgres/evaluate.js +17 -5
- package/dist/postgres/index.d.ts +32 -11
- package/dist/postgres/index.js +65 -31
- package/dist/postgres/migrations.d.ts +60 -0
- package/dist/postgres/migrations.js +121 -0
- package/dist/postgres/operations.d.ts +14 -3
- package/dist/postgres/operations.js +45 -15
- package/dist/postgres/prompts.d.ts +11 -2
- package/dist/postgres/prompts.js +14 -3
- package/dist/postgres/rollups.d.ts +35 -0
- package/dist/postgres/rollups.js +129 -0
- package/dist/postgres/store.d.ts +11 -2
- package/dist/postgres/store.js +14 -3
- package/dist/postgres/traces.d.ts +13 -3
- package/dist/postgres/traces.js +25 -3
- package/dist/postgres/vectors.d.ts +11 -2
- package/dist/postgres/vectors.js +14 -3
- package/dist/router/auto-router.d.ts +34 -7
- package/dist/router/auto-router.js +201 -68
- package/dist/server/assistant.d.ts +44 -0
- package/dist/server/assistant.js +39 -0
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/runs.d.ts +28 -1
- package/dist/server/runs.js +110 -14
- package/dist/sqlite/index.d.ts +16 -3
- package/dist/sqlite/index.js +16 -3
- package/dist/sqlite/migrations.d.ts +26 -0
- package/dist/sqlite/migrations.js +63 -0
- package/dist/sqlite/operations.d.ts +14 -3
- package/dist/sqlite/operations.js +42 -15
- package/dist/sqlite/store.d.ts +9 -2
- package/dist/sqlite/store.js +13 -3
- package/dist/sqlite/vectors.d.ts +11 -2
- package/dist/sqlite/vectors.js +13 -2
- package/dist/store/tenant.d.ts +12 -0
- package/dist/store/tenant.js +29 -0
- package/dist/tenancy/index.d.ts +113 -0
- package/dist/tenancy/index.js +233 -0
- package/dist/testing/record.d.ts +42 -0
- package/dist/testing/record.js +57 -5
- package/dist/tracing/index.d.ts +3 -3
- package/dist/tracing/index.js +2 -2
- package/dist/tracing/instrument.js +30 -0
- package/dist/tracing/otlp.d.ts +91 -0
- package/dist/tracing/otlp.js +124 -0
- package/dist/tracing/rollups.d.ts +100 -0
- package/dist/tracing/rollups.js +139 -0
- package/dist/tracing/stores.d.ts +17 -0
- package/dist/tracing/stores.js +23 -0
- package/dist/tracing/tracer.d.ts +50 -4
- package/dist/tracing/tracer.js +85 -6
- package/dist/types/config.d.ts +33 -2
- package/dist/types/evaluate.d.ts +37 -0
- package/dist/types/graph.d.ts +354 -9
- package/dist/types/graph.js +3 -1
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/operations.d.ts +22 -2
- package/dist/types/response.d.ts +20 -0
- package/dist/types/server.d.ts +18 -0
- package/dist/types/tracing.d.ts +11 -0
- package/dist/utils/schema-migrations.d.ts +97 -0
- package/dist/utils/schema-migrations.js +91 -0
- package/dist/utils/signals.d.ts +17 -0
- package/dist/utils/signals.js +26 -0
- package/dist/utils/tenant.d.ts +14 -0
- package/dist/utils/tenant.js +16 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +1 -0
- package/dist-cjs/agent/create-agent.d.ts +9 -1
- package/dist-cjs/agent/create-agent.js +40 -1
- package/dist-cjs/cache/memory-cache.d.ts +8 -0
- package/dist-cjs/cache/memory-cache.js +9 -0
- package/dist-cjs/cache/semantic-cache.d.ts +6 -3
- package/dist-cjs/cache/semantic-cache.js +5 -2
- package/dist-cjs/cli/args.js +3 -0
- package/dist-cjs/cli/db.d.ts +16 -4
- package/dist-cjs/cli/db.js +200 -20
- package/dist-cjs/cli/doctor.d.ts +17 -0
- package/dist-cjs/cli/doctor.js +56 -0
- package/dist-cjs/cli/graph.d.ts +9 -0
- package/dist-cjs/cli/graph.js +50 -0
- package/dist-cjs/core/lifecycle.js +11 -6
- package/dist-cjs/core/nexus.d.ts +12 -2
- package/dist-cjs/core/nexus.js +106 -23
- package/dist-cjs/doctor/index.d.ts +90 -0
- package/dist-cjs/doctor/index.js +276 -0
- package/dist-cjs/evals/judge.d.ts +11 -0
- package/dist-cjs/evals/judge.js +33 -0
- package/dist-cjs/evaluate/index.d.ts +1 -0
- package/dist-cjs/evaluate/index.js +3 -1
- package/dist-cjs/evaluate/provenance.d.ts +22 -0
- package/dist-cjs/evaluate/provenance.js +14 -0
- package/dist-cjs/evaluate/run.js +20 -4
- package/dist-cjs/graph/durability.d.ts +27 -0
- package/dist-cjs/graph/durability.js +70 -0
- package/dist-cjs/graph/errors.d.ts +30 -4
- package/dist-cjs/graph/errors.js +20 -3
- package/dist-cjs/graph/event-stream.d.ts +75 -0
- package/dist-cjs/graph/event-stream.js +295 -0
- package/dist-cjs/graph/functional.d.ts +80 -7
- package/dist-cjs/graph/functional.js +137 -34
- package/dist-cjs/graph/graph.d.ts +56 -8
- package/dist-cjs/graph/graph.js +330 -48
- package/dist-cjs/graph/index.d.ts +4 -2
- package/dist-cjs/graph/index.js +7 -1
- package/dist-cjs/graph/lint.d.ts +57 -0
- package/dist-cjs/graph/lint.js +144 -0
- package/dist-cjs/graph/run-control.d.ts +28 -0
- package/dist-cjs/graph/run-control.js +30 -0
- package/dist-cjs/images/evals.js +4 -2
- package/dist-cjs/operations/adapters.d.ts +52 -4
- package/dist-cjs/operations/adapters.js +193 -6
- package/dist-cjs/operations/runner.js +14 -15
- package/dist-cjs/operations/stats.js +8 -1
- package/dist-cjs/operations/store.d.ts +5 -1
- package/dist-cjs/operations/store.js +7 -0
- package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
- package/dist-cjs/ops/circuit-breaker.js +4 -0
- package/dist-cjs/ops/circuit-store.d.ts +13 -2
- package/dist-cjs/ops/circuit-store.js +68 -0
- package/dist-cjs/ops/circuit-sync.d.ts +17 -0
- package/dist-cjs/ops/circuit-sync.js +87 -0
- package/dist-cjs/ops/health.d.ts +24 -0
- package/dist-cjs/ops/health.js +38 -10
- package/dist-cjs/ops/index.d.ts +1 -1
- package/dist-cjs/ops/rate-limit-adapters.d.ts +23 -0
- package/dist-cjs/ops/rate-limit-adapters.js +35 -1
- package/dist-cjs/ops/rate-limiter.d.ts +14 -2
- package/dist-cjs/ops/rate-limiter.js +32 -0
- package/dist-cjs/postgres/circuits.d.ts +21 -4
- package/dist-cjs/postgres/circuits.js +98 -3
- package/dist-cjs/postgres/client.d.ts +0 -2
- package/dist-cjs/postgres/client.js +0 -5
- package/dist-cjs/postgres/evaluate.d.ts +16 -4
- package/dist-cjs/postgres/evaluate.js +50 -4
- package/dist-cjs/postgres/index.d.ts +32 -11
- package/dist-cjs/postgres/index.js +69 -20
- package/dist-cjs/postgres/migrations.d.ts +60 -0
- package/dist-cjs/postgres/migrations.js +129 -0
- package/dist-cjs/postgres/operations.d.ts +14 -3
- package/dist-cjs/postgres/operations.js +78 -14
- package/dist-cjs/postgres/prompts.d.ts +11 -2
- package/dist-cjs/postgres/prompts.js +47 -2
- package/dist-cjs/postgres/rollups.d.ts +35 -0
- package/dist-cjs/postgres/rollups.js +168 -0
- package/dist-cjs/postgres/store.d.ts +11 -2
- package/dist-cjs/postgres/store.js +47 -2
- package/dist-cjs/postgres/traces.d.ts +13 -3
- package/dist-cjs/postgres/traces.js +58 -2
- package/dist-cjs/postgres/vectors.d.ts +11 -2
- package/dist-cjs/postgres/vectors.js +47 -2
- package/dist-cjs/router/auto-router.d.ts +34 -7
- package/dist-cjs/router/auto-router.js +201 -68
- package/dist-cjs/server/assistant.d.ts +44 -0
- package/dist-cjs/server/assistant.js +39 -0
- package/dist-cjs/server/errors.d.ts +12 -0
- package/dist-cjs/server/errors.js +9 -1
- package/dist-cjs/server/index.d.ts +1 -1
- package/dist-cjs/server/index.js +2 -1
- package/dist-cjs/server/runs.d.ts +28 -1
- package/dist-cjs/server/runs.js +109 -13
- package/dist-cjs/sqlite/index.d.ts +16 -3
- package/dist-cjs/sqlite/index.js +31 -10
- package/dist-cjs/sqlite/migrations.d.ts +26 -0
- package/dist-cjs/sqlite/migrations.js +71 -0
- package/dist-cjs/sqlite/operations.d.ts +14 -3
- package/dist-cjs/sqlite/operations.js +76 -15
- package/dist-cjs/sqlite/store.d.ts +9 -2
- package/dist-cjs/sqlite/store.js +47 -3
- package/dist-cjs/sqlite/vectors.d.ts +11 -2
- package/dist-cjs/sqlite/vectors.js +47 -2
- package/dist-cjs/store/tenant.d.ts +12 -0
- package/dist-cjs/store/tenant.js +33 -0
- package/dist-cjs/tenancy/index.d.ts +113 -0
- package/dist-cjs/tenancy/index.js +250 -0
- package/dist-cjs/testing/record.d.ts +42 -0
- package/dist-cjs/testing/record.js +57 -5
- package/dist-cjs/tracing/index.d.ts +3 -3
- package/dist-cjs/tracing/index.js +4 -1
- package/dist-cjs/tracing/instrument.js +30 -0
- package/dist-cjs/tracing/otlp.d.ts +91 -0
- package/dist-cjs/tracing/otlp.js +129 -0
- package/dist-cjs/tracing/rollups.d.ts +100 -0
- package/dist-cjs/tracing/rollups.js +147 -0
- package/dist-cjs/tracing/stores.d.ts +17 -0
- package/dist-cjs/tracing/stores.js +24 -0
- package/dist-cjs/tracing/tracer.d.ts +50 -4
- package/dist-cjs/tracing/tracer.js +87 -6
- package/dist-cjs/types/config.d.ts +33 -2
- package/dist-cjs/types/evaluate.d.ts +37 -0
- package/dist-cjs/types/graph.d.ts +354 -9
- package/dist-cjs/types/graph.js +3 -1
- package/dist-cjs/types/messages.d.ts +5 -0
- package/dist-cjs/types/operations.d.ts +22 -2
- package/dist-cjs/types/response.d.ts +20 -0
- package/dist-cjs/types/server.d.ts +18 -0
- package/dist-cjs/types/tracing.d.ts +11 -0
- package/dist-cjs/utils/schema-migrations.d.ts +97 -0
- package/dist-cjs/utils/schema-migrations.js +102 -0
- package/dist-cjs/utils/signals.d.ts +17 -0
- package/dist-cjs/utils/signals.js +29 -0
- package/dist-cjs/utils/tenant.d.ts +14 -0
- package/dist-cjs/utils/tenant.js +22 -0
- package/dist-cjs/version.d.ts +5 -0
- package/dist-cjs/version.js +4 -0
- package/llms.txt +4 -3
- package/package.json +110 -2
|
@@ -26,6 +26,8 @@ export declare class CircuitCoordinator {
|
|
|
26
26
|
private lastSyncAt;
|
|
27
27
|
/** False after a store failure, until a sync succeeds; the breaker then decides locally. */
|
|
28
28
|
private reachable;
|
|
29
|
+
/** Calls not yet added to the shared window, per provider, with `shareObservations`. */
|
|
30
|
+
private readonly unreported;
|
|
29
31
|
constructor(host: CircuitHost);
|
|
30
32
|
/**
|
|
31
33
|
* Whether this worker may go half-open once a cooldown has elapsed. With a reachable store only
|
|
@@ -35,6 +37,11 @@ export declare class CircuitCoordinator {
|
|
|
35
37
|
/** Starts a background sync when the last one is older than the interval. Never waits. */
|
|
36
38
|
tick(): void;
|
|
37
39
|
sync(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Counts one call toward the shared window, to be added on the next sync. Cheap and synchronous:
|
|
42
|
+
* a check never waits for the store.
|
|
43
|
+
*/
|
|
44
|
+
observed(providerName: string, failed: boolean): void;
|
|
38
45
|
/** Publishes a transition this worker made. */
|
|
39
46
|
published(providerName: string, entry: CircuitEntry, to: 'open' | 'closed', reason: string): void;
|
|
40
47
|
/** Publishes an operator reset, so every worker closes the circuit rather than only this one. */
|
|
@@ -45,6 +52,14 @@ export declare class CircuitCoordinator {
|
|
|
45
52
|
*/
|
|
46
53
|
reconcile(): void;
|
|
47
54
|
flush(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Adds this worker's unreported calls to each provider's shared window, then opens a closed circuit
|
|
57
|
+
* whose shared count crossed a threshold. The batch is taken before the first await, so calls made
|
|
58
|
+
* while it is in flight wait for the next sync rather than being lost or counted twice.
|
|
59
|
+
*/
|
|
60
|
+
private report;
|
|
61
|
+
/** Opens a closed circuit whose shared window crossed the breaker's thresholds. */
|
|
62
|
+
private judge;
|
|
48
63
|
/** Takes a decision another worker published, when it is newer than what this worker knows. */
|
|
49
64
|
private adopt;
|
|
50
65
|
/**
|
|
@@ -55,3 +70,5 @@ export declare class CircuitCoordinator {
|
|
|
55
70
|
private publish;
|
|
56
71
|
private failed;
|
|
57
72
|
}
|
|
73
|
+
/** The width of one shared-window bucket: a twelfth of the window, so a bucket rolling out moves the count by little. */
|
|
74
|
+
export declare function bucketWidth(windowMs: number): number;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CircuitCoordinator = void 0;
|
|
4
|
+
exports.bucketWidth = bucketWidth;
|
|
4
5
|
class CircuitCoordinator {
|
|
5
6
|
host;
|
|
6
7
|
pending = new Set();
|
|
7
8
|
syncing;
|
|
8
9
|
lastSyncAt = Number.NEGATIVE_INFINITY;
|
|
9
10
|
reachable = true;
|
|
11
|
+
unreported = new Map();
|
|
10
12
|
constructor(host) {
|
|
11
13
|
this.host = host;
|
|
12
14
|
}
|
|
@@ -29,6 +31,8 @@ class CircuitCoordinator {
|
|
|
29
31
|
this.syncing = (async () => {
|
|
30
32
|
try {
|
|
31
33
|
this.lastSyncAt = this.host.now();
|
|
34
|
+
if (this.unreported.size > 0)
|
|
35
|
+
await this.report(store);
|
|
32
36
|
for (const shared of await store.read())
|
|
33
37
|
this.adopt(shared);
|
|
34
38
|
const resetTimeoutMs = this.host.config.resetTimeoutMs ?? 30_000;
|
|
@@ -54,6 +58,29 @@ class CircuitCoordinator {
|
|
|
54
58
|
})();
|
|
55
59
|
return this.syncing;
|
|
56
60
|
}
|
|
61
|
+
observed(providerName, failed) {
|
|
62
|
+
if (!this.host.config.shareObservations || !this.host.config.store?.observe)
|
|
63
|
+
return;
|
|
64
|
+
const bucketMs = bucketWidth(this.host.config.windowMs ?? 60_000);
|
|
65
|
+
const at = Math.floor(this.host.now() / bucketMs) * bucketMs;
|
|
66
|
+
let pending = this.unreported.get(providerName);
|
|
67
|
+
if (!pending) {
|
|
68
|
+
pending = { buckets: new Map(), succeeded: false, trailing: 0 };
|
|
69
|
+
this.unreported.set(providerName, pending);
|
|
70
|
+
}
|
|
71
|
+
const bucket = pending.buckets.get(at) ?? { calls: 0, failures: 0 };
|
|
72
|
+
bucket.calls += 1;
|
|
73
|
+
if (failed)
|
|
74
|
+
bucket.failures += 1;
|
|
75
|
+
pending.buckets.set(at, bucket);
|
|
76
|
+
if (failed)
|
|
77
|
+
pending.trailing += 1;
|
|
78
|
+
else {
|
|
79
|
+
pending.succeeded = true;
|
|
80
|
+
pending.trailing = 0;
|
|
81
|
+
}
|
|
82
|
+
this.tick();
|
|
83
|
+
}
|
|
57
84
|
published(providerName, entry, to, reason) {
|
|
58
85
|
entry.changedAt = this.stampAfter(entry.changedAt);
|
|
59
86
|
this.publish({
|
|
@@ -75,6 +102,23 @@ class CircuitCoordinator {
|
|
|
75
102
|
reconcile() {
|
|
76
103
|
for (const name of [...this.host.names()]) {
|
|
77
104
|
const entry = this.host.entry(name);
|
|
105
|
+
if (this.host.config.shareObservations && this.host.config.store?.observe && entry.window.length > 0) {
|
|
106
|
+
const bucketMs = bucketWidth(this.host.config.windowMs ?? 60_000);
|
|
107
|
+
const buckets = new Map();
|
|
108
|
+
for (const call of entry.window) {
|
|
109
|
+
const at = Math.floor(call.at / bucketMs) * bucketMs;
|
|
110
|
+
const bucket = buckets.get(at) ?? { calls: 0, failures: 0 };
|
|
111
|
+
bucket.calls += 1;
|
|
112
|
+
if (call.failed)
|
|
113
|
+
bucket.failures += 1;
|
|
114
|
+
buckets.set(at, bucket);
|
|
115
|
+
}
|
|
116
|
+
this.unreported.set(name, {
|
|
117
|
+
buckets,
|
|
118
|
+
succeeded: entry.window.some((call) => !call.failed),
|
|
119
|
+
trailing: entry.consecutiveFailures,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
78
122
|
if (entry.state === 'open' && entry.changedAt === Number.NEGATIVE_INFINITY) {
|
|
79
123
|
this.published(name, entry, 'open', entry.reason ?? 'opened while shared state was loading');
|
|
80
124
|
}
|
|
@@ -84,6 +128,46 @@ class CircuitCoordinator {
|
|
|
84
128
|
async flush() {
|
|
85
129
|
await Promise.all([...this.pending]);
|
|
86
130
|
}
|
|
131
|
+
async report(store) {
|
|
132
|
+
if (!store.observe || this.unreported.size === 0)
|
|
133
|
+
return;
|
|
134
|
+
const batches = [...this.unreported];
|
|
135
|
+
this.unreported.clear();
|
|
136
|
+
const windowMs = this.host.config.windowMs ?? 60_000;
|
|
137
|
+
for (const [name, pending] of batches) {
|
|
138
|
+
const entry = this.host.entry(name);
|
|
139
|
+
const window = await store.observe(name, {
|
|
140
|
+
buckets: [...pending.buckets].map(([at, bucket]) => ({ at, calls: bucket.calls, failures: bucket.failures })),
|
|
141
|
+
succeeded: pending.succeeded,
|
|
142
|
+
trailingFailures: pending.trailing,
|
|
143
|
+
windowMs,
|
|
144
|
+
bucketMs: bucketWidth(windowMs),
|
|
145
|
+
now: this.host.now(),
|
|
146
|
+
...(entry.state === 'closed' && Number.isFinite(entry.changedAt) ? { since: entry.changedAt } : {}),
|
|
147
|
+
});
|
|
148
|
+
entry.shared = {
|
|
149
|
+
calls: window.calls,
|
|
150
|
+
failures: window.failures,
|
|
151
|
+
consecutiveFailures: window.consecutiveFailures,
|
|
152
|
+
};
|
|
153
|
+
this.judge(name, entry, window);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
judge(name, entry, window) {
|
|
157
|
+
if (entry.state !== 'closed')
|
|
158
|
+
return;
|
|
159
|
+
const config = this.host.config;
|
|
160
|
+
if (window.consecutiveFailures >= (config.failureThreshold ?? 5)) {
|
|
161
|
+
this.host.transition(name, entry, 'open', 'shared failure threshold reached');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const rate = config.failureRateThreshold;
|
|
165
|
+
if (rate !== undefined &&
|
|
166
|
+
window.calls >= (config.minimumThroughput ?? 10) &&
|
|
167
|
+
window.failures / window.calls >= rate) {
|
|
168
|
+
this.host.transition(name, entry, 'open', 'shared failure rate threshold reached');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
87
171
|
adopt(shared) {
|
|
88
172
|
const entry = this.host.entry(shared.providerName);
|
|
89
173
|
if (shared.updatedAt <= entry.changedAt)
|
|
@@ -124,3 +208,6 @@ class CircuitCoordinator {
|
|
|
124
208
|
}
|
|
125
209
|
}
|
|
126
210
|
exports.CircuitCoordinator = CircuitCoordinator;
|
|
211
|
+
function bucketWidth(windowMs) {
|
|
212
|
+
return Math.max(1, Math.floor(windowMs / 12));
|
|
213
|
+
}
|
package/dist-cjs/ops/health.d.ts
CHANGED
|
@@ -21,7 +21,18 @@ export interface ProviderHealthSnapshot {
|
|
|
21
21
|
* latency up to 30.
|
|
22
22
|
*/
|
|
23
23
|
score: number;
|
|
24
|
+
/**
|
|
25
|
+
* `unknown` with no recent calls to judge by, `unhealthy` past the failure threshold or under the
|
|
26
|
+
* minimum score, `degraded` when calls are failing or slow but not enough to stop routing to it,
|
|
27
|
+
* and `healthy` otherwise. Always set by `ProviderHealthMonitor`; optional so a hand-built snapshot
|
|
28
|
+
* still type-checks.
|
|
29
|
+
*/
|
|
30
|
+
status?: ProviderHealthStatus;
|
|
31
|
+
/** True when the last call is older than `observationTtlMs`, so the figures above are old news. */
|
|
32
|
+
stale?: boolean;
|
|
24
33
|
}
|
|
34
|
+
/** Where a provider stands, by what its recent calls say. */
|
|
35
|
+
export type ProviderHealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
|
25
36
|
/** Health tracking for providers, used to route around unhealthy ones. */
|
|
26
37
|
export interface HealthConfig {
|
|
27
38
|
/** Turns tracking on. Off by default, and then nothing is recorded. */
|
|
@@ -30,12 +41,22 @@ export interface HealthConfig {
|
|
|
30
41
|
failureThreshold?: number;
|
|
31
42
|
/** Score below which a provider is unhealthy. Defaults to 20. */
|
|
32
43
|
minScore?: number;
|
|
44
|
+
/**
|
|
45
|
+
* How long a call's outcome counts, in milliseconds. A provider whose last call is older is
|
|
46
|
+
* `unknown`, and is routed to as a provider never seen is, rather than kept down for a failure
|
|
47
|
+
* hours ago or kept up for a success days ago. `checkProviders({ staleOnly: true })` refreshes it.
|
|
48
|
+
* Defaults to never expiring, as before.
|
|
49
|
+
*/
|
|
50
|
+
observationTtlMs?: number;
|
|
51
|
+
/** Replaces the system clock, for tests. */
|
|
52
|
+
now?: () => Date;
|
|
33
53
|
}
|
|
34
54
|
/** Tracks provider health from call outcomes and scores each provider for routing. */
|
|
35
55
|
export declare class ProviderHealthMonitor {
|
|
36
56
|
private config;
|
|
37
57
|
private states;
|
|
38
58
|
constructor(config?: HealthConfig);
|
|
59
|
+
private nowDate;
|
|
39
60
|
/** Records a successful call and its latency. */
|
|
40
61
|
recordSuccess(providerName: string, latencyMs: number): void;
|
|
41
62
|
/** Records a failed call. */
|
|
@@ -46,6 +67,9 @@ export declare class ProviderHealthMonitor {
|
|
|
46
67
|
score(providerName: string): number;
|
|
47
68
|
/** Whether a provider is healthy. */
|
|
48
69
|
isHealthy(providerName: string): boolean;
|
|
70
|
+
/** Whether a provider's figures are too old to judge by, or there are none. */
|
|
71
|
+
isUnknown(providerName: string): boolean;
|
|
72
|
+
private isStale;
|
|
49
73
|
private snapshotOne;
|
|
50
74
|
private state;
|
|
51
75
|
}
|
package/dist-cjs/ops/health.js
CHANGED
|
@@ -7,23 +7,30 @@ class ProviderHealthMonitor {
|
|
|
7
7
|
constructor(config = {}) {
|
|
8
8
|
this.config = config;
|
|
9
9
|
}
|
|
10
|
+
nowDate() {
|
|
11
|
+
return this.config.now?.() ?? new Date();
|
|
12
|
+
}
|
|
10
13
|
recordSuccess(providerName, latencyMs) {
|
|
11
14
|
if (!this.config.enabled)
|
|
12
15
|
return;
|
|
13
16
|
const state = this.state(providerName);
|
|
14
17
|
state.successes += 1;
|
|
15
18
|
state.consecutiveFailures = 0;
|
|
19
|
+
if (this.isStale(state))
|
|
20
|
+
Object.assign(state, freshState());
|
|
16
21
|
state.avgLatencyMs = state.avgLatencyMs ? state.avgLatencyMs * 0.7 + latencyMs * 0.3 : latencyMs;
|
|
17
|
-
state.lastCheckedAt =
|
|
22
|
+
state.lastCheckedAt = this.nowDate().toISOString();
|
|
18
23
|
}
|
|
19
24
|
recordFailure(providerName, error) {
|
|
20
25
|
if (!this.config.enabled)
|
|
21
26
|
return;
|
|
22
27
|
const state = this.state(providerName);
|
|
28
|
+
if (this.isStale(state))
|
|
29
|
+
state.consecutiveFailures = 0;
|
|
23
30
|
state.failures += 1;
|
|
24
31
|
state.consecutiveFailures += 1;
|
|
25
32
|
state.lastError = error instanceof Error ? error.message : String(error);
|
|
26
|
-
state.lastCheckedAt =
|
|
33
|
+
state.lastCheckedAt = this.nowDate().toISOString();
|
|
27
34
|
}
|
|
28
35
|
snapshot(providerName) {
|
|
29
36
|
const names = providerName ? [providerName] : [...this.states.keys()];
|
|
@@ -35,16 +42,39 @@ class ProviderHealthMonitor {
|
|
|
35
42
|
isHealthy(providerName) {
|
|
36
43
|
return this.snapshotOne(providerName).healthy;
|
|
37
44
|
}
|
|
45
|
+
isUnknown(providerName) {
|
|
46
|
+
return this.snapshotOne(providerName).status === 'unknown';
|
|
47
|
+
}
|
|
48
|
+
isStale(state) {
|
|
49
|
+
const ttl = this.config.observationTtlMs;
|
|
50
|
+
if (ttl === undefined || !state.lastCheckedAt)
|
|
51
|
+
return false;
|
|
52
|
+
return this.nowDate().getTime() - Date.parse(state.lastCheckedAt) > ttl;
|
|
53
|
+
}
|
|
38
54
|
snapshotOne(providerName) {
|
|
39
|
-
const
|
|
55
|
+
const recorded = this.state(providerName);
|
|
56
|
+
const stale = this.isStale(recorded);
|
|
57
|
+
const state = stale
|
|
58
|
+
? { ...freshState(), lastError: recorded.lastError, lastCheckedAt: recorded.lastCheckedAt }
|
|
59
|
+
: recorded;
|
|
40
60
|
const failureThreshold = this.config.failureThreshold || 3;
|
|
41
61
|
const failurePenalty = Math.min(60, state.consecutiveFailures * 20);
|
|
42
62
|
const latencyPenalty = Math.min(30, state.avgLatencyMs / 1000);
|
|
43
63
|
const score = Math.max(0, 100 - failurePenalty - latencyPenalty);
|
|
44
64
|
const minScore = this.config.minScore ?? 20;
|
|
65
|
+
const healthy = state.consecutiveFailures < failureThreshold && score >= minScore;
|
|
66
|
+
const seen = recorded.successes + recorded.failures > 0;
|
|
45
67
|
return {
|
|
46
68
|
providerName,
|
|
47
|
-
healthy
|
|
69
|
+
healthy,
|
|
70
|
+
status: stale || !seen
|
|
71
|
+
? 'unknown'
|
|
72
|
+
: !healthy
|
|
73
|
+
? 'unhealthy'
|
|
74
|
+
: state.consecutiveFailures > 0 || score < 70
|
|
75
|
+
? 'degraded'
|
|
76
|
+
: 'healthy',
|
|
77
|
+
stale,
|
|
48
78
|
successes: state.successes,
|
|
49
79
|
failures: state.failures,
|
|
50
80
|
consecutiveFailures: state.consecutiveFailures,
|
|
@@ -57,15 +87,13 @@ class ProviderHealthMonitor {
|
|
|
57
87
|
state(providerName) {
|
|
58
88
|
let state = this.states.get(providerName);
|
|
59
89
|
if (!state) {
|
|
60
|
-
state =
|
|
61
|
-
successes: 0,
|
|
62
|
-
failures: 0,
|
|
63
|
-
consecutiveFailures: 0,
|
|
64
|
-
avgLatencyMs: 0,
|
|
65
|
-
};
|
|
90
|
+
state = freshState();
|
|
66
91
|
this.states.set(providerName, state);
|
|
67
92
|
}
|
|
68
93
|
return state;
|
|
69
94
|
}
|
|
70
95
|
}
|
|
71
96
|
exports.ProviderHealthMonitor = ProviderHealthMonitor;
|
|
97
|
+
function freshState() {
|
|
98
|
+
return { successes: 0, failures: 0, consecutiveFailures: 0, avgLatencyMs: 0 };
|
|
99
|
+
}
|
package/dist-cjs/ops/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { AuditLogger } from './audit-logger.js';
|
|
8
8
|
export { type FamilyCallDescriptor, type FamilyRuntime, FamilyTelemetry } from './family-telemetry.js';
|
|
9
|
-
export { type HealthConfig, ProviderHealthMonitor, type ProviderHealthSnapshot } from './health.js';
|
|
9
|
+
export { type HealthConfig, ProviderHealthMonitor, type ProviderHealthSnapshot, type ProviderHealthStatus, } from './health.js';
|
|
10
10
|
export { InMemoryMetrics, type MetricsConfig, MetricsCollector, type MetricsSink, type OpenTelemetryLikeMeter, OpenTelemetryMetricsSink, } from './metrics.js';
|
|
11
11
|
export { type OpenTelemetryLikeSpan, type OpenTelemetryLikeTracer, OpenTelemetryTraceExporter, } from './otel-tracing.js';
|
|
12
12
|
export { NexusRateLimitError, type RateLimitedRequest, RateLimiter } from './rate-limiter.js';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { gcraDecide } from './rate-limiter.js';
|
|
2
|
+
export { gcraDecide };
|
|
1
3
|
/** A rate-limit window after counting one call. */
|
|
2
4
|
export interface RateLimitHit {
|
|
3
5
|
/** Calls counted in the current window, including this one. */
|
|
@@ -5,6 +7,13 @@ export interface RateLimitHit {
|
|
|
5
7
|
/** Epoch milliseconds when the window resets. */
|
|
6
8
|
resetAt: number;
|
|
7
9
|
}
|
|
10
|
+
/** The verdict on one call under GCRA. */
|
|
11
|
+
export interface RateLimitDecision {
|
|
12
|
+
/** Whether the call may go. */
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
/** Epoch milliseconds from which a refused call would be allowed. */
|
|
15
|
+
retryAt: number;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Where rate-limit counters live.
|
|
10
19
|
*
|
|
@@ -16,14 +25,22 @@ export interface RateLimitStore {
|
|
|
16
25
|
hit(key: string, windowMs: number): Promise<RateLimitHit> | RateLimitHit;
|
|
17
26
|
/** Resets a key's window. */
|
|
18
27
|
reset?(key: string): Promise<void> | void;
|
|
28
|
+
/**
|
|
29
|
+
* Decides one call under GCRA: one call per `emissionMs` on average, with up to `burst` at once,
|
|
30
|
+
* atomically. Needed for `algorithm: 'gcra'`; both bundled stores have it.
|
|
31
|
+
*/
|
|
32
|
+
gcra?(key: string, emissionMs: number, burst: number): Promise<RateLimitDecision> | RateLimitDecision;
|
|
19
33
|
}
|
|
20
34
|
/** Process-local counters. The default, and equivalent to the limiter's built-in behavior. */
|
|
21
35
|
export declare class MemoryRateLimitStore implements RateLimitStore {
|
|
22
36
|
private readonly now;
|
|
23
37
|
private readonly buckets;
|
|
38
|
+
private readonly arrivals;
|
|
24
39
|
constructor(now?: () => number);
|
|
25
40
|
/** Counts one call, starting a new window when the last has ended. */
|
|
26
41
|
hit(key: string, windowMs: number): RateLimitHit;
|
|
42
|
+
/** Decides one call under GCRA. */
|
|
43
|
+
gcra(key: string, emissionMs: number, burst: number): RateLimitDecision;
|
|
27
44
|
/** Resets a key's window. */
|
|
28
45
|
reset(key: string): void;
|
|
29
46
|
/** Resets every window. */
|
|
@@ -65,11 +82,17 @@ export interface RedisRateLimitStoreOptions {
|
|
|
65
82
|
*/
|
|
66
83
|
export declare class RedisRateLimitStore implements RateLimitStore {
|
|
67
84
|
private readonly client;
|
|
85
|
+
private readonly options;
|
|
68
86
|
private readonly prefix;
|
|
69
87
|
private readonly useEval;
|
|
70
88
|
constructor(client: RedisRateLimitLikeClient, options?: RedisRateLimitStoreOptions);
|
|
71
89
|
/** Counts one call, in one atomic step when the client has `eval`. */
|
|
72
90
|
hit(key: string, windowMs: number): Promise<RateLimitHit>;
|
|
91
|
+
/**
|
|
92
|
+
* Decides one call under GCRA, atomically, on Redis's own clock. Needs a client with `eval`: the
|
|
93
|
+
* read and the write cannot be split without letting two workers both take the last slot.
|
|
94
|
+
*/
|
|
95
|
+
gcra(key: string, emissionMs: number, burst: number): Promise<RateLimitDecision>;
|
|
73
96
|
/** Resets a key's window. Does nothing when the client has no `del`. */
|
|
74
97
|
reset(key: string): Promise<void>;
|
|
75
98
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RedisRateLimitStore = exports.MemoryRateLimitStore = void 0;
|
|
3
|
+
exports.RedisRateLimitStore = exports.MemoryRateLimitStore = exports.gcraDecide = void 0;
|
|
4
|
+
const rate_limiter_js_1 = require("./rate-limiter.js");
|
|
5
|
+
Object.defineProperty(exports, "gcraDecide", { enumerable: true, get: function () { return rate_limiter_js_1.gcraDecide; } });
|
|
4
6
|
class MemoryRateLimitStore {
|
|
5
7
|
now;
|
|
6
8
|
buckets = new Map();
|
|
9
|
+
arrivals = new Map();
|
|
7
10
|
constructor(now = () => Date.now()) {
|
|
8
11
|
this.now = now;
|
|
9
12
|
}
|
|
@@ -18,11 +21,19 @@ class MemoryRateLimitStore {
|
|
|
18
21
|
bucket.count += 1;
|
|
19
22
|
return bucket;
|
|
20
23
|
}
|
|
24
|
+
gcra(key, emissionMs, burst) {
|
|
25
|
+
const decided = (0, rate_limiter_js_1.gcraDecide)(this.arrivals.get(key), this.now(), emissionMs, burst);
|
|
26
|
+
if (decided.allowed)
|
|
27
|
+
this.arrivals.set(key, decided.tat);
|
|
28
|
+
return { allowed: decided.allowed, retryAt: decided.retryAt };
|
|
29
|
+
}
|
|
21
30
|
reset(key) {
|
|
22
31
|
this.buckets.delete(key);
|
|
32
|
+
this.arrivals.delete(key);
|
|
23
33
|
}
|
|
24
34
|
clear() {
|
|
25
35
|
this.buckets.clear();
|
|
36
|
+
this.arrivals.clear();
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
@@ -39,12 +50,27 @@ if ttl < 0 then
|
|
|
39
50
|
end
|
|
40
51
|
return {count, ttl}
|
|
41
52
|
`;
|
|
53
|
+
const GCRA_SCRIPT = `
|
|
54
|
+
local time = redis.call('TIME')
|
|
55
|
+
local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
56
|
+
local emission = tonumber(ARGV[1])
|
|
57
|
+
local burst = tonumber(ARGV[2])
|
|
58
|
+
local tat = tonumber(redis.call('GET', KEYS[1]) or now)
|
|
59
|
+
if tat < now then tat = now end
|
|
60
|
+
local next = tat + emission
|
|
61
|
+
local allowAt = next - burst * emission
|
|
62
|
+
if allowAt > now then return {0, allowAt - now} end
|
|
63
|
+
redis.call('SET', KEYS[1], next, 'PX', math.max(1, math.ceil(next - now)))
|
|
64
|
+
return {1, 0}
|
|
65
|
+
`;
|
|
42
66
|
class RedisRateLimitStore {
|
|
43
67
|
client;
|
|
68
|
+
options;
|
|
44
69
|
prefix;
|
|
45
70
|
useEval;
|
|
46
71
|
constructor(client, options = {}) {
|
|
47
72
|
this.client = client;
|
|
73
|
+
this.options = options;
|
|
48
74
|
this.prefix = options.prefix ?? 'nexus-ai-pro:ratelimit:';
|
|
49
75
|
this.useEval = options.useEval !== false && typeof client.eval === 'function';
|
|
50
76
|
}
|
|
@@ -68,6 +94,14 @@ class RedisRateLimitStore {
|
|
|
68
94
|
}
|
|
69
95
|
return { count, resetAt: Date.now() + ttl };
|
|
70
96
|
}
|
|
97
|
+
async gcra(key, emissionMs, burst) {
|
|
98
|
+
if (!this.client.eval || this.options.useEval === false) {
|
|
99
|
+
throw new Error('GCRA rate limiting through Redis needs a client with eval');
|
|
100
|
+
}
|
|
101
|
+
const raw = (await this.client.eval(GCRA_SCRIPT, 1, `${this.prefix}gcra:${key}`, String(emissionMs), String(burst)));
|
|
102
|
+
const allowed = Number(raw?.[0]) === 1;
|
|
103
|
+
return { allowed, retryAt: Date.now() + Math.max(0, Number(raw?.[1] ?? 0)) };
|
|
104
|
+
}
|
|
71
105
|
async reset(key) {
|
|
72
106
|
await this.client.del?.(this.prefix + key);
|
|
73
107
|
}
|
|
@@ -10,6 +10,8 @@ export interface RateLimitedRequest {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
/** Caller, for per-user limits. */
|
|
12
12
|
userId?: string;
|
|
13
|
+
/** Tenant, for per-tenant limits. */
|
|
14
|
+
tenantId?: string;
|
|
13
15
|
}
|
|
14
16
|
/** Raised when a caller exceeds its rate limit. */
|
|
15
17
|
export declare class NexusRateLimitError extends Error {
|
|
@@ -26,11 +28,12 @@ export declare class NexusRateLimitError extends Error {
|
|
|
26
28
|
get retryAfterSeconds(): number | undefined;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* Rate limiting per user, per model, or globally, in memory or through a shared store, by fixed
|
|
32
|
+
* windows or by GCRA.
|
|
31
33
|
*/
|
|
32
34
|
export declare class RateLimiter {
|
|
33
35
|
private buckets;
|
|
36
|
+
private arrivals;
|
|
34
37
|
/**
|
|
35
38
|
* Counts one call against a distributed store.
|
|
36
39
|
*
|
|
@@ -43,3 +46,12 @@ export declare class RateLimiter {
|
|
|
43
46
|
check(request: RateLimitedRequest, config?: RateLimitConfig): void;
|
|
44
47
|
private getKey;
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* GCRA on one key's theoretical arrival time: the time the key would be idle again at the allowed
|
|
51
|
+
* rate. A call is allowed when that time, after counting it, is no more than `burst` intervals ahead.
|
|
52
|
+
*/
|
|
53
|
+
export declare function gcraDecide(tat: number | undefined, now: number, emissionMs: number, burst: number): {
|
|
54
|
+
allowed: boolean;
|
|
55
|
+
retryAt: number;
|
|
56
|
+
tat: number;
|
|
57
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RateLimiter = exports.NexusRateLimitError = void 0;
|
|
4
|
+
exports.gcraDecide = gcraDecide;
|
|
4
5
|
class NexusRateLimitError extends Error {
|
|
5
6
|
key;
|
|
6
7
|
resetAt;
|
|
@@ -19,6 +20,7 @@ class NexusRateLimitError extends Error {
|
|
|
19
20
|
exports.NexusRateLimitError = NexusRateLimitError;
|
|
20
21
|
class RateLimiter {
|
|
21
22
|
buckets = new Map();
|
|
23
|
+
arrivals = new Map();
|
|
22
24
|
async checkAsync(request, config) {
|
|
23
25
|
if (!config?.enabled)
|
|
24
26
|
return;
|
|
@@ -28,6 +30,14 @@ class RateLimiter {
|
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
32
|
const key = this.getKey(request, config);
|
|
33
|
+
if (config.algorithm === 'gcra') {
|
|
34
|
+
if (!store.gcra)
|
|
35
|
+
throw new Error('This rate-limit store cannot decide GCRA; give it gcra(), or use fixed windows');
|
|
36
|
+
const decided = await store.gcra(key, emissionOf(config), burstOf(config));
|
|
37
|
+
if (!decided.allowed)
|
|
38
|
+
throw new NexusRateLimitError(key, decided.retryAt);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
31
41
|
const hit = await store.hit(key, config.windowMs);
|
|
32
42
|
if (hit.count > config.maxRequests) {
|
|
33
43
|
throw new NexusRateLimitError(key, hit.resetAt);
|
|
@@ -38,6 +48,13 @@ class RateLimiter {
|
|
|
38
48
|
return;
|
|
39
49
|
const key = this.getKey(request, config);
|
|
40
50
|
const now = Date.now();
|
|
51
|
+
if (config.algorithm === 'gcra') {
|
|
52
|
+
const decided = gcraDecide(this.arrivals.get(key), now, emissionOf(config), burstOf(config));
|
|
53
|
+
if (!decided.allowed)
|
|
54
|
+
throw new NexusRateLimitError(key, decided.retryAt);
|
|
55
|
+
this.arrivals.set(key, decided.tat);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
41
58
|
const bucket = this.buckets.get(key);
|
|
42
59
|
if (!bucket || bucket.resetAt <= now) {
|
|
43
60
|
this.buckets.set(key, { count: 1, resetAt: now + config.windowMs });
|
|
@@ -51,9 +68,24 @@ class RateLimiter {
|
|
|
51
68
|
getKey(request, config) {
|
|
52
69
|
if (config.key === 'model')
|
|
53
70
|
return `model:${request.model}`;
|
|
71
|
+
if (config.key === 'tenantId')
|
|
72
|
+
return `tenant:${request.tenantId || 'none'}`;
|
|
54
73
|
if (config.key === 'global')
|
|
55
74
|
return 'global';
|
|
56
75
|
return `user:${request.userId || 'anonymous'}`;
|
|
57
76
|
}
|
|
58
77
|
}
|
|
59
78
|
exports.RateLimiter = RateLimiter;
|
|
79
|
+
function emissionOf(config) {
|
|
80
|
+
return config.windowMs / Math.max(1, config.maxRequests);
|
|
81
|
+
}
|
|
82
|
+
function burstOf(config) {
|
|
83
|
+
return Math.max(1, config.burst ?? config.maxRequests);
|
|
84
|
+
}
|
|
85
|
+
function gcraDecide(tat, now, emissionMs, burst) {
|
|
86
|
+
const next = Math.max(tat ?? now, now) + emissionMs;
|
|
87
|
+
const allowAt = next - burst * emissionMs;
|
|
88
|
+
if (allowAt > now)
|
|
89
|
+
return { allowed: false, retryAt: allowAt, tat: tat ?? now };
|
|
90
|
+
return { allowed: true, retryAt: now, tat: next };
|
|
91
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { CircuitStateStore, SharedCircuitState } from '../ops/circuit-breaker.js';
|
|
1
|
+
import type { CircuitObservation, CircuitStateStore, CircuitWindow, SharedCircuitState } from '../ops/circuit-breaker.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
2
3
|
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
3
5
|
/** Options for the Postgres circuit store. */
|
|
4
6
|
export interface PostgresCircuitStateStoreOptions {
|
|
5
7
|
/** Table name, optionally schema-qualified. Defaults to `nexus_circuits`. */
|
|
@@ -7,8 +9,13 @@ export interface PostgresCircuitStateStoreOptions {
|
|
|
7
9
|
/** Clock for probe leases. Defaults to `Date.now`, the same clock the breaker uses. */
|
|
8
10
|
now?: () => number;
|
|
9
11
|
}
|
|
10
|
-
/** The schema, as statements. */
|
|
12
|
+
/** The schema, as statements: every migration's, in order. */
|
|
11
13
|
export declare function circuitStoreMigration(options?: Pick<PostgresCircuitStateStoreOptions, 'table'>): string[];
|
|
14
|
+
/**
|
|
15
|
+
* The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 table;
|
|
16
|
+
* version 2 adds the shared failure window, so a breaker counts every worker's calls.
|
|
17
|
+
*/
|
|
18
|
+
export declare function circuitStoreMigrations(options?: Pick<PostgresCircuitStateStoreOptions, 'table'>): SchemaMigration[];
|
|
12
19
|
/**
|
|
13
20
|
* Shared circuit state in Postgres.
|
|
14
21
|
*
|
|
@@ -20,10 +27,14 @@ export declare class PostgresCircuitStateStore implements CircuitStateStore {
|
|
|
20
27
|
private readonly client;
|
|
21
28
|
private readonly options;
|
|
22
29
|
private readonly table;
|
|
30
|
+
private readonly windowTable;
|
|
23
31
|
private readonly now;
|
|
24
32
|
constructor(client: PostgresLikeClient, options?: PostgresCircuitStateStoreOptions);
|
|
25
|
-
/**
|
|
26
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
35
|
+
* Never runs implicitly. Safe on a database an older release created: what exists is kept.
|
|
36
|
+
*/
|
|
37
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
27
38
|
/** Every provider's shared circuit state. */
|
|
28
39
|
read(): Promise<SharedCircuitState[]>;
|
|
29
40
|
/**
|
|
@@ -35,4 +46,10 @@ export declare class PostgresCircuitStateStore implements CircuitStateStore {
|
|
|
35
46
|
* claimant, including when it already holds the claim.
|
|
36
47
|
*/
|
|
37
48
|
claimProbe(providerName: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Adds one worker's calls to a provider's shared window, then sums it. The add, the trim of buckets
|
|
51
|
+
* that rolled out, and the streak update are one statement, so two workers reporting at once both
|
|
52
|
+
* count. Needs version 2 of the schema.
|
|
53
|
+
*/
|
|
54
|
+
observe(providerName: string, observation: CircuitObservation): Promise<CircuitWindow>;
|
|
38
55
|
}
|