ai-sdk-rate-limiter 0.9.0 → 0.10.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/README.md +8 -1
- package/dist/errors-DcXM0HCM.d.cts +68 -0
- package/dist/errors-DcXM0HCM.d.ts +68 -0
- package/dist/index.d.cts +2 -68
- package/dist/index.d.ts +2 -68
- package/dist/middleware.cjs +228 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +198 -0
- package/dist/middleware.d.ts +198 -0
- package/dist/middleware.js +223 -0
- package/dist/middleware.js.map +1 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -1431,7 +1431,14 @@ import type { StatsDClient } from 'ai-sdk-rate-limiter/statsd'
|
|
|
1431
1431
|
|
|
1432
1432
|
## Examples
|
|
1433
1433
|
|
|
1434
|
-
|
|
1434
|
+
Four runnable examples are included, each with its own README:
|
|
1435
|
+
|
|
1436
|
+
| Example | What it shows |
|
|
1437
|
+
|---|---|
|
|
1438
|
+
| [`examples/nextjs/`](./examples/nextjs/) | Next.js 15 App Router streaming chat — rate limiting, live cost display, budget error handling |
|
|
1439
|
+
| [`examples/multi-tenant-express/`](./examples/multi-tenant-express/) | Express API with per-user isolated limits (free/pro tiers), per-user cost reports, circuit breaker |
|
|
1440
|
+
| [`examples/batch-processing/`](./examples/batch-processing/) | Classify 30+ items concurrently without 429s — priority queuing, graceful shutdown, live cost tracking |
|
|
1441
|
+
| [`examples/budget-alerts/`](./examples/budget-alerts/) | Slack/webhook alerts on budget thresholds — instant `budgetHit` events + periodic spend summaries |
|
|
1435
1442
|
|
|
1436
1443
|
---
|
|
1437
1444
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Base class for all ai-sdk-rate-limiter errors */
|
|
2
|
+
declare class RateLimiterError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a request cannot proceed because the rate limit was hit
|
|
8
|
+
* and the request either timed out waiting in the queue or exhausted all retries.
|
|
9
|
+
*/
|
|
10
|
+
declare class RateLimitExceededError extends RateLimiterError {
|
|
11
|
+
readonly model: string;
|
|
12
|
+
readonly limitType: 'rpm' | 'itpm' | 'otpm';
|
|
13
|
+
readonly limit: number;
|
|
14
|
+
readonly resetAt: number;
|
|
15
|
+
constructor(model: string, limitType: 'rpm' | 'itpm' | 'otpm', limit: number, resetAt: number);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a request has waited in the queue longer than the configured timeout.
|
|
19
|
+
*/
|
|
20
|
+
declare class QueueTimeoutError extends RateLimiterError {
|
|
21
|
+
readonly model: string;
|
|
22
|
+
readonly waitedMs: number;
|
|
23
|
+
readonly queueDepth: number;
|
|
24
|
+
constructor(model: string, waitedMs: number, queueDepth: number);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when a new request arrives and the queue is at capacity.
|
|
28
|
+
*/
|
|
29
|
+
declare class QueueFullError extends RateLimiterError {
|
|
30
|
+
readonly model: string;
|
|
31
|
+
readonly maxSize: number;
|
|
32
|
+
constructor(model: string, maxSize: number);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when a request would exceed the configured cost budget.
|
|
36
|
+
*/
|
|
37
|
+
declare class BudgetExceededError extends RateLimiterError {
|
|
38
|
+
readonly model: string;
|
|
39
|
+
readonly currentCostUsd: number;
|
|
40
|
+
readonly limitUsd: number;
|
|
41
|
+
readonly period: 'hourly' | 'daily' | 'monthly';
|
|
42
|
+
constructor(model: string, currentCostUsd: number, limitUsd: number, period: 'hourly' | 'daily' | 'monthly');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when a request is blocked because the circuit breaker is open.
|
|
46
|
+
*/
|
|
47
|
+
declare class CircuitOpenError extends RateLimiterError {
|
|
48
|
+
readonly model: string;
|
|
49
|
+
readonly openUntilMs: number;
|
|
50
|
+
constructor(model: string, openUntilMs: number);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when a request arrives after shutdown() has been called.
|
|
54
|
+
*/
|
|
55
|
+
declare class ShutdownError extends RateLimiterError {
|
|
56
|
+
constructor();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when all retry attempts are exhausted.
|
|
60
|
+
*/
|
|
61
|
+
declare class RetryExhaustedError extends RateLimiterError {
|
|
62
|
+
readonly model: string;
|
|
63
|
+
readonly attempts: number;
|
|
64
|
+
readonly cause: unknown;
|
|
65
|
+
constructor(model: string, attempts: number, cause: unknown);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { BudgetExceededError as B, CircuitOpenError as C, QueueFullError as Q, RateLimitExceededError as R, ShutdownError as S, QueueTimeoutError as a, RateLimiterError as b, RetryExhaustedError as c };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Base class for all ai-sdk-rate-limiter errors */
|
|
2
|
+
declare class RateLimiterError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a request cannot proceed because the rate limit was hit
|
|
8
|
+
* and the request either timed out waiting in the queue or exhausted all retries.
|
|
9
|
+
*/
|
|
10
|
+
declare class RateLimitExceededError extends RateLimiterError {
|
|
11
|
+
readonly model: string;
|
|
12
|
+
readonly limitType: 'rpm' | 'itpm' | 'otpm';
|
|
13
|
+
readonly limit: number;
|
|
14
|
+
readonly resetAt: number;
|
|
15
|
+
constructor(model: string, limitType: 'rpm' | 'itpm' | 'otpm', limit: number, resetAt: number);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a request has waited in the queue longer than the configured timeout.
|
|
19
|
+
*/
|
|
20
|
+
declare class QueueTimeoutError extends RateLimiterError {
|
|
21
|
+
readonly model: string;
|
|
22
|
+
readonly waitedMs: number;
|
|
23
|
+
readonly queueDepth: number;
|
|
24
|
+
constructor(model: string, waitedMs: number, queueDepth: number);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when a new request arrives and the queue is at capacity.
|
|
28
|
+
*/
|
|
29
|
+
declare class QueueFullError extends RateLimiterError {
|
|
30
|
+
readonly model: string;
|
|
31
|
+
readonly maxSize: number;
|
|
32
|
+
constructor(model: string, maxSize: number);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when a request would exceed the configured cost budget.
|
|
36
|
+
*/
|
|
37
|
+
declare class BudgetExceededError extends RateLimiterError {
|
|
38
|
+
readonly model: string;
|
|
39
|
+
readonly currentCostUsd: number;
|
|
40
|
+
readonly limitUsd: number;
|
|
41
|
+
readonly period: 'hourly' | 'daily' | 'monthly';
|
|
42
|
+
constructor(model: string, currentCostUsd: number, limitUsd: number, period: 'hourly' | 'daily' | 'monthly');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when a request is blocked because the circuit breaker is open.
|
|
46
|
+
*/
|
|
47
|
+
declare class CircuitOpenError extends RateLimiterError {
|
|
48
|
+
readonly model: string;
|
|
49
|
+
readonly openUntilMs: number;
|
|
50
|
+
constructor(model: string, openUntilMs: number);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when a request arrives after shutdown() has been called.
|
|
54
|
+
*/
|
|
55
|
+
declare class ShutdownError extends RateLimiterError {
|
|
56
|
+
constructor();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when all retry attempts are exhausted.
|
|
60
|
+
*/
|
|
61
|
+
declare class RetryExhaustedError extends RateLimiterError {
|
|
62
|
+
readonly model: string;
|
|
63
|
+
readonly attempts: number;
|
|
64
|
+
readonly cause: unknown;
|
|
65
|
+
constructor(model: string, attempts: number, cause: unknown);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { BudgetExceededError as B, CircuitOpenError as C, QueueFullError as Q, RateLimitExceededError as R, ShutdownError as S, QueueTimeoutError as a, RateLimiterError as b, RetryExhaustedError as c };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { R as RateLimiterConfig, a as RateLimiter, P as Priority, M as ModelLimitOverride, b as ModelLimits } from './types-CUPpMRPE.cjs';
|
|
2
2
|
export { B as BackoffStrategy, c as BudgetExceededAction, d as BudgetHitEvent, e as BudgetPeriod, C as CircuitBreakerConfig, f as CircuitClosedEvent, g as CircuitOpenEvent, h as CompletedEvent, i as CostConfig, j as CostReport, k as CostStore, D as DequeuedEvent, l as DroppedEvent, E as EventHandler, m as EventHandlers, n as EventMap, L as LimiterStatus, o as LimitsDetectedEvent, p as ModelStatus, q as PerRequestOptions, r as PeriodCostSummary, s as PersistedCostEntry, Q as QueueConfig, t as QueuedEvent, u as RateLimitStore, v as RateLimitedEvent, w as RetryConfig, x as RetryingEvent, S as ScopeConfig } from './types-CUPpMRPE.cjs';
|
|
3
|
+
export { B as BudgetExceededError, C as CircuitOpenError, Q as QueueFullError, a as QueueTimeoutError, R as RateLimitExceededError, b as RateLimiterError, c as RetryExhaustedError, S as ShutdownError } from './errors-DcXM0HCM.cjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Create a rate limiter instance.
|
|
@@ -78,73 +79,6 @@ interface RawSdkProxyOptions {
|
|
|
78
79
|
*/
|
|
79
80
|
declare function rateLimited<T extends object>(client: T, options?: RawSdkProxyOptions): T;
|
|
80
81
|
|
|
81
|
-
/** Base class for all ai-sdk-rate-limiter errors */
|
|
82
|
-
declare class RateLimiterError extends Error {
|
|
83
|
-
name: string;
|
|
84
|
-
constructor(message: string);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Thrown when a request cannot proceed because the rate limit was hit
|
|
88
|
-
* and the request either timed out waiting in the queue or exhausted all retries.
|
|
89
|
-
*/
|
|
90
|
-
declare class RateLimitExceededError extends RateLimiterError {
|
|
91
|
-
readonly model: string;
|
|
92
|
-
readonly limitType: 'rpm' | 'itpm' | 'otpm';
|
|
93
|
-
readonly limit: number;
|
|
94
|
-
readonly resetAt: number;
|
|
95
|
-
constructor(model: string, limitType: 'rpm' | 'itpm' | 'otpm', limit: number, resetAt: number);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Thrown when a request has waited in the queue longer than the configured timeout.
|
|
99
|
-
*/
|
|
100
|
-
declare class QueueTimeoutError extends RateLimiterError {
|
|
101
|
-
readonly model: string;
|
|
102
|
-
readonly waitedMs: number;
|
|
103
|
-
readonly queueDepth: number;
|
|
104
|
-
constructor(model: string, waitedMs: number, queueDepth: number);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Thrown when a new request arrives and the queue is at capacity.
|
|
108
|
-
*/
|
|
109
|
-
declare class QueueFullError extends RateLimiterError {
|
|
110
|
-
readonly model: string;
|
|
111
|
-
readonly maxSize: number;
|
|
112
|
-
constructor(model: string, maxSize: number);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Thrown when a request would exceed the configured cost budget.
|
|
116
|
-
*/
|
|
117
|
-
declare class BudgetExceededError extends RateLimiterError {
|
|
118
|
-
readonly model: string;
|
|
119
|
-
readonly currentCostUsd: number;
|
|
120
|
-
readonly limitUsd: number;
|
|
121
|
-
readonly period: 'hourly' | 'daily' | 'monthly';
|
|
122
|
-
constructor(model: string, currentCostUsd: number, limitUsd: number, period: 'hourly' | 'daily' | 'monthly');
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Thrown when a request is blocked because the circuit breaker is open.
|
|
126
|
-
*/
|
|
127
|
-
declare class CircuitOpenError extends RateLimiterError {
|
|
128
|
-
readonly model: string;
|
|
129
|
-
readonly openUntilMs: number;
|
|
130
|
-
constructor(model: string, openUntilMs: number);
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Thrown when a request arrives after shutdown() has been called.
|
|
134
|
-
*/
|
|
135
|
-
declare class ShutdownError extends RateLimiterError {
|
|
136
|
-
constructor();
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Thrown when all retry attempts are exhausted.
|
|
140
|
-
*/
|
|
141
|
-
declare class RetryExhaustedError extends RateLimiterError {
|
|
142
|
-
readonly model: string;
|
|
143
|
-
readonly attempts: number;
|
|
144
|
-
readonly cause: unknown;
|
|
145
|
-
constructor(model: string, attempts: number, cause: unknown);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
82
|
/**
|
|
149
83
|
* Look up rate limits and pricing for a model.
|
|
150
84
|
* Resolution order:
|
|
@@ -238,4 +172,4 @@ declare const MISTRAL_MODELS: Record<string, ModelLimits>;
|
|
|
238
172
|
*/
|
|
239
173
|
declare const COHERE_MODELS: Record<string, ModelLimits>;
|
|
240
174
|
|
|
241
|
-
export { ANTHROPIC_MODELS,
|
|
175
|
+
export { ANTHROPIC_MODELS, COHERE_MODELS, GOOGLE_MODELS, GROQ_MODELS, MISTRAL_MODELS, ModelLimitOverride, ModelLimits, OPENAI_MODELS, Priority, RateLimiter, RateLimiterConfig, type RawSdkProxyOptions, createRateLimiter, isKnownModel, rateLimited, resolveModelLimits };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { R as RateLimiterConfig, a as RateLimiter, P as Priority, M as ModelLimitOverride, b as ModelLimits } from './types-CUPpMRPE.js';
|
|
2
2
|
export { B as BackoffStrategy, c as BudgetExceededAction, d as BudgetHitEvent, e as BudgetPeriod, C as CircuitBreakerConfig, f as CircuitClosedEvent, g as CircuitOpenEvent, h as CompletedEvent, i as CostConfig, j as CostReport, k as CostStore, D as DequeuedEvent, l as DroppedEvent, E as EventHandler, m as EventHandlers, n as EventMap, L as LimiterStatus, o as LimitsDetectedEvent, p as ModelStatus, q as PerRequestOptions, r as PeriodCostSummary, s as PersistedCostEntry, Q as QueueConfig, t as QueuedEvent, u as RateLimitStore, v as RateLimitedEvent, w as RetryConfig, x as RetryingEvent, S as ScopeConfig } from './types-CUPpMRPE.js';
|
|
3
|
+
export { B as BudgetExceededError, C as CircuitOpenError, Q as QueueFullError, a as QueueTimeoutError, R as RateLimitExceededError, b as RateLimiterError, c as RetryExhaustedError, S as ShutdownError } from './errors-DcXM0HCM.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Create a rate limiter instance.
|
|
@@ -78,73 +79,6 @@ interface RawSdkProxyOptions {
|
|
|
78
79
|
*/
|
|
79
80
|
declare function rateLimited<T extends object>(client: T, options?: RawSdkProxyOptions): T;
|
|
80
81
|
|
|
81
|
-
/** Base class for all ai-sdk-rate-limiter errors */
|
|
82
|
-
declare class RateLimiterError extends Error {
|
|
83
|
-
name: string;
|
|
84
|
-
constructor(message: string);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Thrown when a request cannot proceed because the rate limit was hit
|
|
88
|
-
* and the request either timed out waiting in the queue or exhausted all retries.
|
|
89
|
-
*/
|
|
90
|
-
declare class RateLimitExceededError extends RateLimiterError {
|
|
91
|
-
readonly model: string;
|
|
92
|
-
readonly limitType: 'rpm' | 'itpm' | 'otpm';
|
|
93
|
-
readonly limit: number;
|
|
94
|
-
readonly resetAt: number;
|
|
95
|
-
constructor(model: string, limitType: 'rpm' | 'itpm' | 'otpm', limit: number, resetAt: number);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Thrown when a request has waited in the queue longer than the configured timeout.
|
|
99
|
-
*/
|
|
100
|
-
declare class QueueTimeoutError extends RateLimiterError {
|
|
101
|
-
readonly model: string;
|
|
102
|
-
readonly waitedMs: number;
|
|
103
|
-
readonly queueDepth: number;
|
|
104
|
-
constructor(model: string, waitedMs: number, queueDepth: number);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Thrown when a new request arrives and the queue is at capacity.
|
|
108
|
-
*/
|
|
109
|
-
declare class QueueFullError extends RateLimiterError {
|
|
110
|
-
readonly model: string;
|
|
111
|
-
readonly maxSize: number;
|
|
112
|
-
constructor(model: string, maxSize: number);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Thrown when a request would exceed the configured cost budget.
|
|
116
|
-
*/
|
|
117
|
-
declare class BudgetExceededError extends RateLimiterError {
|
|
118
|
-
readonly model: string;
|
|
119
|
-
readonly currentCostUsd: number;
|
|
120
|
-
readonly limitUsd: number;
|
|
121
|
-
readonly period: 'hourly' | 'daily' | 'monthly';
|
|
122
|
-
constructor(model: string, currentCostUsd: number, limitUsd: number, period: 'hourly' | 'daily' | 'monthly');
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Thrown when a request is blocked because the circuit breaker is open.
|
|
126
|
-
*/
|
|
127
|
-
declare class CircuitOpenError extends RateLimiterError {
|
|
128
|
-
readonly model: string;
|
|
129
|
-
readonly openUntilMs: number;
|
|
130
|
-
constructor(model: string, openUntilMs: number);
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Thrown when a request arrives after shutdown() has been called.
|
|
134
|
-
*/
|
|
135
|
-
declare class ShutdownError extends RateLimiterError {
|
|
136
|
-
constructor();
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Thrown when all retry attempts are exhausted.
|
|
140
|
-
*/
|
|
141
|
-
declare class RetryExhaustedError extends RateLimiterError {
|
|
142
|
-
readonly model: string;
|
|
143
|
-
readonly attempts: number;
|
|
144
|
-
readonly cause: unknown;
|
|
145
|
-
constructor(model: string, attempts: number, cause: unknown);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
82
|
/**
|
|
149
83
|
* Look up rate limits and pricing for a model.
|
|
150
84
|
* Resolution order:
|
|
@@ -238,4 +172,4 @@ declare const MISTRAL_MODELS: Record<string, ModelLimits>;
|
|
|
238
172
|
*/
|
|
239
173
|
declare const COHERE_MODELS: Record<string, ModelLimits>;
|
|
240
174
|
|
|
241
|
-
export { ANTHROPIC_MODELS,
|
|
175
|
+
export { ANTHROPIC_MODELS, COHERE_MODELS, GOOGLE_MODELS, GROQ_MODELS, MISTRAL_MODELS, ModelLimitOverride, ModelLimits, OPENAI_MODELS, Priority, RateLimiter, RateLimiterConfig, type RawSdkProxyOptions, createRateLimiter, isKnownModel, rateLimited, resolveModelLimits };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var RateLimiterError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RateLimiterError";
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var QueueTimeoutError = class extends RateLimiterError {
|
|
12
|
+
constructor(model, waitedMs, queueDepth) {
|
|
13
|
+
super(
|
|
14
|
+
`Request for model "${model}" timed out after waiting ${waitedMs}ms in the queue (current queue depth: ${queueDepth}).`
|
|
15
|
+
);
|
|
16
|
+
this.model = model;
|
|
17
|
+
this.waitedMs = waitedMs;
|
|
18
|
+
this.queueDepth = queueDepth;
|
|
19
|
+
this.name = "QueueTimeoutError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var QueueFullError = class extends RateLimiterError {
|
|
23
|
+
constructor(model, maxSize) {
|
|
24
|
+
super(
|
|
25
|
+
`Queue for model "${model}" is full (maxSize: ${maxSize}). Increase queue.maxSize or reduce request rate.`
|
|
26
|
+
);
|
|
27
|
+
this.model = model;
|
|
28
|
+
this.maxSize = maxSize;
|
|
29
|
+
this.name = "QueueFullError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var BudgetExceededError = class extends RateLimiterError {
|
|
33
|
+
constructor(model, currentCostUsd, limitUsd, period) {
|
|
34
|
+
super(
|
|
35
|
+
`Cost budget exceeded for model "${model}": $${currentCostUsd.toFixed(4)} used of $${limitUsd.toFixed(2)} ${period} budget.`
|
|
36
|
+
);
|
|
37
|
+
this.model = model;
|
|
38
|
+
this.currentCostUsd = currentCostUsd;
|
|
39
|
+
this.limitUsd = limitUsd;
|
|
40
|
+
this.period = period;
|
|
41
|
+
this.name = "BudgetExceededError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var CircuitOpenError = class extends RateLimiterError {
|
|
45
|
+
constructor(model, openUntilMs) {
|
|
46
|
+
super(
|
|
47
|
+
`Circuit breaker for model "${model}" is open due to repeated failures. Requests are blocked until ${new Date(openUntilMs).toISOString()}.`
|
|
48
|
+
);
|
|
49
|
+
this.model = model;
|
|
50
|
+
this.openUntilMs = openUntilMs;
|
|
51
|
+
this.name = "CircuitOpenError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ShutdownError = class extends RateLimiterError {
|
|
55
|
+
constructor() {
|
|
56
|
+
super("Rate limiter is shutting down \u2014 new requests are not accepted.");
|
|
57
|
+
this.name = "ShutdownError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/middleware.ts
|
|
62
|
+
function createRateLimiterMiddleware(limiter, options = {}) {
|
|
63
|
+
const middleware = (req, res, next) => {
|
|
64
|
+
const scope = options.scope?.(req);
|
|
65
|
+
const priority = typeof options.priority === "function" ? options.priority(req) : options.priority;
|
|
66
|
+
const ctx = {
|
|
67
|
+
...scope !== void 0 && { scope },
|
|
68
|
+
...priority !== void 0 && { priority }
|
|
69
|
+
};
|
|
70
|
+
req["rateLimiter"] = ctx;
|
|
71
|
+
if (options.injectHeaders && !res.headersSent) {
|
|
72
|
+
const modelId = typeof options.injectHeaders === "function" ? options.injectHeaders(req) : options.injectHeaders;
|
|
73
|
+
const status = limiter.getStatus();
|
|
74
|
+
const modelStat = status.models.find((m) => m.modelId === modelId);
|
|
75
|
+
if (modelStat) {
|
|
76
|
+
res.setHeader("X-RateLimit-Model", modelId);
|
|
77
|
+
res.setHeader("X-RateLimit-Queue-Depth", modelStat.queueDepth);
|
|
78
|
+
res.setHeader("X-RateLimit-Requests-Window", modelStat.requestsInWindow);
|
|
79
|
+
if (modelStat.estimatedWaitMs > 0) {
|
|
80
|
+
res.setHeader("X-RateLimit-Estimated-Wait-Ms", modelStat.estimatedWaitMs);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
next();
|
|
85
|
+
};
|
|
86
|
+
const errorHandler = (err, _req, res, next) => {
|
|
87
|
+
if (!(err instanceof RateLimiterError)) {
|
|
88
|
+
next(err);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (res.headersSent) {
|
|
92
|
+
next(err);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const { status, body } = mapErrorToResponse(err);
|
|
96
|
+
res.status(status).json(body);
|
|
97
|
+
};
|
|
98
|
+
return { middleware, errorHandler };
|
|
99
|
+
}
|
|
100
|
+
function createRateLimiterErrorHandler(options = {}) {
|
|
101
|
+
return (err, _req, res, next) => {
|
|
102
|
+
if (!(err instanceof RateLimiterError)) {
|
|
103
|
+
next(err);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (res.headersSent) {
|
|
107
|
+
next(err);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (options.format) {
|
|
111
|
+
const custom = options.format(err);
|
|
112
|
+
if (custom == null) {
|
|
113
|
+
next(err);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.status(custom.status).json(custom.body);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const { status, body } = mapErrorToResponse(err, options.includeDetails);
|
|
120
|
+
res.status(status).json(body);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function createHonoMiddleware(limiter, options = {}) {
|
|
124
|
+
return async (c, next) => {
|
|
125
|
+
const scope = options.scope?.(c);
|
|
126
|
+
const priority = typeof options.priority === "function" ? options.priority(c) : options.priority;
|
|
127
|
+
const ctx = {
|
|
128
|
+
...scope !== void 0 && { scope },
|
|
129
|
+
...priority !== void 0 && { priority }
|
|
130
|
+
};
|
|
131
|
+
c.set("rateLimiter", ctx);
|
|
132
|
+
if (options.injectHeaders) {
|
|
133
|
+
const modelId = typeof options.injectHeaders === "function" ? options.injectHeaders(c) : options.injectHeaders;
|
|
134
|
+
const status = limiter.getStatus();
|
|
135
|
+
const modelStat = status.models.find((m) => m.modelId === modelId);
|
|
136
|
+
if (modelStat) {
|
|
137
|
+
c.header("X-RateLimit-Model", modelId);
|
|
138
|
+
c.header("X-RateLimit-Queue-Depth", String(modelStat.queueDepth));
|
|
139
|
+
c.header("X-RateLimit-Requests-Window", String(modelStat.requestsInWindow));
|
|
140
|
+
if (modelStat.estimatedWaitMs > 0) {
|
|
141
|
+
c.header("X-RateLimit-Estimated-Wait-Ms", String(modelStat.estimatedWaitMs));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await next();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof RateLimiterError) {
|
|
149
|
+
const { status, body } = mapErrorToResponse(err);
|
|
150
|
+
return c.json(body, status);
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function mapErrorToResponse(err, includeDetails = true) {
|
|
157
|
+
if (err instanceof QueueTimeoutError) {
|
|
158
|
+
return {
|
|
159
|
+
status: 503,
|
|
160
|
+
body: {
|
|
161
|
+
error: "Request queued too long. Try again shortly.",
|
|
162
|
+
code: "QUEUE_TIMEOUT",
|
|
163
|
+
...includeDetails && {
|
|
164
|
+
retryAfterMs: 5e3,
|
|
165
|
+
queueDepth: err.queueDepth
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (err instanceof QueueFullError) {
|
|
171
|
+
return {
|
|
172
|
+
status: 503,
|
|
173
|
+
body: {
|
|
174
|
+
error: "Server is busy. Try again in a moment.",
|
|
175
|
+
code: "QUEUE_FULL"
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (err instanceof BudgetExceededError) {
|
|
180
|
+
return {
|
|
181
|
+
status: 402,
|
|
182
|
+
body: {
|
|
183
|
+
error: "AI usage budget exceeded.",
|
|
184
|
+
code: "BUDGET_EXCEEDED",
|
|
185
|
+
...includeDetails && {
|
|
186
|
+
period: err.period,
|
|
187
|
+
limitUsd: err.limitUsd,
|
|
188
|
+
currentCostUsd: err.currentCostUsd
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (err instanceof CircuitOpenError) {
|
|
194
|
+
return {
|
|
195
|
+
status: 503,
|
|
196
|
+
body: {
|
|
197
|
+
error: "AI provider temporarily unavailable.",
|
|
198
|
+
code: "CIRCUIT_OPEN",
|
|
199
|
+
...includeDetails && {
|
|
200
|
+
retryAfter: Math.max(0, Math.ceil((err.openUntilMs - Date.now()) / 1e3))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (err instanceof ShutdownError) {
|
|
206
|
+
return {
|
|
207
|
+
status: 503,
|
|
208
|
+
body: {
|
|
209
|
+
error: "Service is shutting down.",
|
|
210
|
+
code: "SHUTDOWN"
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
status: 429,
|
|
216
|
+
body: {
|
|
217
|
+
error: "Rate limit exceeded.",
|
|
218
|
+
code: "RATE_LIMITED"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
exports.createHonoMiddleware = createHonoMiddleware;
|
|
224
|
+
exports.createRateLimiterErrorHandler = createRateLimiterErrorHandler;
|
|
225
|
+
exports.createRateLimiterMiddleware = createRateLimiterMiddleware;
|
|
226
|
+
exports.mapErrorToResponse = mapErrorToResponse;
|
|
227
|
+
//# sourceMappingURL=middleware.cjs.map
|
|
228
|
+
//# sourceMappingURL=middleware.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/middleware.ts"],"names":[],"mappings":"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","file":"middleware.cjs","sourcesContent":["/** Base class for all ai-sdk-rate-limiter errors */\nexport class RateLimiterError extends Error {\n // Declared as mutable string so subclasses can assign in constructors\n declare name: string\n\n constructor(message: string) {\n super(message)\n this.name = 'RateLimiterError'\n // Restore prototype chain (needed when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/**\n * Thrown when a request cannot proceed because the rate limit was hit\n * and the request either timed out waiting in the queue or exhausted all retries.\n */\nexport class RateLimitExceededError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly limitType: 'rpm' | 'itpm' | 'otpm',\n public readonly limit: number,\n public readonly resetAt: number,\n ) {\n super(\n `Rate limit exceeded for model \"${model}\": ${limitType.toUpperCase()} limit of ${limit} hit. ` +\n `Resets at ${new Date(resetAt).toISOString()}.`,\n )\n this.name = 'RateLimitExceededError'\n }\n}\n\n/**\n * Thrown when a request has waited in the queue longer than the configured timeout.\n */\nexport class QueueTimeoutError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly waitedMs: number,\n public readonly queueDepth: number,\n ) {\n super(\n `Request for model \"${model}\" timed out after waiting ${waitedMs}ms in the queue ` +\n `(current queue depth: ${queueDepth}).`,\n )\n this.name = 'QueueTimeoutError'\n }\n}\n\n/**\n * Thrown when a new request arrives and the queue is at capacity.\n */\nexport class QueueFullError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly maxSize: number,\n ) {\n super(\n `Queue for model \"${model}\" is full (maxSize: ${maxSize}). ` +\n `Increase queue.maxSize or reduce request rate.`,\n )\n this.name = 'QueueFullError'\n }\n}\n\n/**\n * Thrown when a request would exceed the configured cost budget.\n */\nexport class BudgetExceededError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly currentCostUsd: number,\n public readonly limitUsd: number,\n public readonly period: 'hourly' | 'daily' | 'monthly',\n ) {\n super(\n `Cost budget exceeded for model \"${model}\": ` +\n `$${currentCostUsd.toFixed(4)} used of $${limitUsd.toFixed(2)} ${period} budget.`,\n )\n this.name = 'BudgetExceededError'\n }\n}\n\n/**\n * Thrown when a request is blocked because the circuit breaker is open.\n */\nexport class CircuitOpenError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly openUntilMs: number,\n ) {\n super(\n `Circuit breaker for model \"${model}\" is open due to repeated failures. ` +\n `Requests are blocked until ${new Date(openUntilMs).toISOString()}.`,\n )\n this.name = 'CircuitOpenError'\n }\n}\n\n/**\n * Thrown when a request arrives after shutdown() has been called.\n */\nexport class ShutdownError extends RateLimiterError {\n constructor() {\n super('Rate limiter is shutting down — new requests are not accepted.')\n this.name = 'ShutdownError'\n }\n}\n\n/**\n * Thrown when all retry attempts are exhausted.\n */\nexport class RetryExhaustedError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly attempts: number,\n public readonly cause: unknown,\n ) {\n super(\n `All ${attempts} retry attempts exhausted for model \"${model}\". ` +\n `Last error: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n this.name = 'RetryExhaustedError'\n if (cause instanceof Error) {\n this.stack = `${this.stack}\\nCaused by: ${cause.stack}`\n }\n }\n}\n","/**\n * ai-sdk-rate-limiter/middleware\n *\n * Framework-agnostic middleware helpers. Reduces per-route boilerplate to zero:\n * scope extraction, priority assignment, and rate-limiter error handling are\n * all handled at the middleware layer.\n *\n * @example Express\n * ```typescript\n * import { createRateLimiterMiddleware } from 'ai-sdk-rate-limiter/middleware'\n *\n * const { middleware, errorHandler } = createRateLimiterMiddleware(limiter, {\n * scope: (req) => `user:${req.headers['x-user-id']}`,\n * })\n *\n * app.use(middleware) // BEFORE routes — attaches req.rateLimiter\n *\n * app.post('/chat', async (req, res) => {\n * await generateText({\n * model,\n * providerOptions: { rateLimiter: req.rateLimiter }, // just pass it through\n * })\n * })\n *\n * app.use(errorHandler) // AFTER routes — converts errors to proper HTTP responses\n * ```\n *\n * @example Hono\n * ```typescript\n * import { createHonoMiddleware } from 'ai-sdk-rate-limiter/middleware'\n *\n * app.use(createHonoMiddleware(limiter, {\n * scope: (c) => c.req.header('x-user-id'),\n * }))\n *\n * app.post('/chat', async (c) => {\n * await generateText({\n * model,\n * providerOptions: { rateLimiter: c.var.rateLimiter },\n * })\n * })\n * ```\n */\n\nimport type { RateLimiter, Priority } from './types.js'\nimport {\n RateLimiterError,\n QueueTimeoutError,\n QueueFullError,\n BudgetExceededError,\n CircuitOpenError,\n ShutdownError,\n} from './errors.js'\n\n// ---------------------------------------------------------------------------\n// Shared request context\n//\n// Stored on req.rateLimiter (Express) or c.var.rateLimiter (Hono).\n// Pass directly to providerOptions.rateLimiter in route handlers.\n// ---------------------------------------------------------------------------\n\nexport interface RateLimiterRequestContext {\n /** Scope key for per-user/org isolated rate limiting */\n scope?: string\n /** Queue priority for this request. Default: 'normal' */\n priority?: Priority\n}\n\n// Augment Node.js http.IncomingMessage so TypeScript knows about req.rateLimiter\n// without requiring users to install @types/express separately.\ndeclare module 'http' {\n interface IncomingMessage {\n /**\n * Populated by createRateLimiterMiddleware(). Pass directly to providerOptions:\n * ```typescript\n * providerOptions: { rateLimiter: req.rateLimiter }\n * ```\n */\n rateLimiter?: RateLimiterRequestContext\n }\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types — no runtime dep on express / hono / fastify\n// ---------------------------------------------------------------------------\n\ninterface MinReq {\n headers: Record<string, string | string[] | undefined>\n [key: string]: unknown\n}\n\ninterface MinRes {\n setHeader(name: string, value: string | number): void\n status(code: number): MinRes\n json(body: unknown): void\n readonly headersSent: boolean\n [key: string]: unknown\n}\n\ntype NextFn = (err?: unknown) => void\n\n// ---------------------------------------------------------------------------\n// Options — Express\n// ---------------------------------------------------------------------------\n\nexport interface RateLimiterMiddlewareOptions {\n /**\n * Extract the per-request scope from the incoming request.\n * Stored in req.rateLimiter.scope.\n *\n * @example (req) => req.headers['x-user-id'] as string\n * @example (req) => `user:${(req as any).user.id}`\n */\n scope?: (req: MinReq) => string | undefined\n\n /**\n * Default queue priority, or derive it per-request.\n * Stored in req.rateLimiter.priority. Default: 'normal'\n *\n * @example (req) => req.headers['x-priority'] === 'high' ? 'high' : 'normal'\n */\n priority?: Priority | ((req: MinReq) => Priority)\n\n /**\n * Inject X-RateLimit-* informational headers into every response.\n * Pass the model ID to inspect, or a function to derive it per-request.\n *\n * @example 'gpt-4o'\n * @example (req) => req.headers['x-ai-model'] as string ?? 'gpt-4o-mini'\n */\n injectHeaders?: string | ((req: MinReq) => string)\n}\n\nexport interface ErrorHandlerOptions {\n /**\n * Include structured details (retryAfter, period, limitUsd…) in the\n * response body. Default: true\n */\n includeDetails?: boolean\n\n /**\n * Override the default error → HTTP mapping.\n * Return null/undefined to fall through to the next error handler.\n */\n format?: (err: RateLimiterError) => { status: number; body: unknown } | null | undefined\n}\n\n// ---------------------------------------------------------------------------\n// Express: createRateLimiterMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a middleware + error handler pair for Express (or any Node.js\n * framework that uses the `(req, res, next)` calling convention).\n *\n * **middleware** — place BEFORE routes. Attaches req.rateLimiter.\n * **errorHandler** — place AFTER routes. Converts RateLimiterErrors to HTTP.\n */\nexport function createRateLimiterMiddleware(\n limiter: RateLimiter,\n options: RateLimiterMiddlewareOptions = {},\n): {\n middleware: (req: MinReq, res: MinRes, next: NextFn) => void\n errorHandler: (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void\n} {\n const middleware = (req: MinReq, res: MinRes, next: NextFn): void => {\n const scope = options.scope?.(req)\n const priority = typeof options.priority === 'function'\n ? options.priority(req)\n : options.priority\n\n const ctx: RateLimiterRequestContext = {\n ...(scope !== undefined && { scope }),\n ...(priority !== undefined && { priority }),\n }\n ;(req as Record<string, unknown>)['rateLimiter'] = ctx\n\n if (options.injectHeaders && !res.headersSent) {\n const modelId = typeof options.injectHeaders === 'function'\n ? options.injectHeaders(req)\n : options.injectHeaders\n const status = limiter.getStatus()\n const modelStat = status.models.find(m => m.modelId === modelId)\n\n if (modelStat) {\n res.setHeader('X-RateLimit-Model', modelId)\n res.setHeader('X-RateLimit-Queue-Depth', modelStat.queueDepth)\n res.setHeader('X-RateLimit-Requests-Window', modelStat.requestsInWindow)\n if (modelStat.estimatedWaitMs > 0) {\n res.setHeader('X-RateLimit-Estimated-Wait-Ms', modelStat.estimatedWaitMs)\n }\n }\n }\n\n next()\n }\n\n const errorHandler = (err: unknown, _req: MinReq, res: MinRes, next: NextFn): void => {\n if (!(err instanceof RateLimiterError)) { next(err); return }\n if (res.headersSent) { next(err); return }\n const { status, body } = mapErrorToResponse(err)\n res.status(status).json(body)\n }\n\n return { middleware, errorHandler }\n}\n\n/**\n * Standalone Express 4-argument error handler.\n * Use this when you only need error handling and not scope injection.\n *\n * @example\n * ```typescript\n * app.use(createRateLimiterErrorHandler({ includeDetails: false }))\n * ```\n */\nexport function createRateLimiterErrorHandler(\n options: ErrorHandlerOptions = {},\n): (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void {\n return (err, _req, res, next) => {\n if (!(err instanceof RateLimiterError)) { next(err); return }\n if (res.headersSent) { next(err); return }\n\n if (options.format) {\n const custom = options.format(err)\n if (custom == null) { next(err); return }\n res.status(custom.status).json(custom.body)\n return\n }\n\n const { status, body } = mapErrorToResponse(err, options.includeDetails)\n res.status(status).json(body)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hono middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal Hono Context interface — structural typing, no hard `hono` dep.\n */\nexport interface HonoContext {\n req: {\n raw: Request\n header(name: string): string | undefined\n }\n set(key: string, value: unknown): void\n json(body: unknown, status?: number): Response\n header(name: string, value: string): void\n var: Record<string, unknown>\n}\n\ntype HonoNext = () => Promise<Response | void>\n\n/** Hono middleware handler signature */\nexport type HonoMiddlewareHandler = (c: HonoContext, next: HonoNext) => Promise<Response | void>\n\nexport interface HonoMiddlewareOptions {\n /**\n * Extract scope from the Hono context. Stored in c.var.rateLimiter.scope.\n *\n * @example (c) => c.req.header('x-user-id')\n * @example (c) => c.var.user?.id ? `user:${c.var.user.id}` : undefined\n */\n scope?: (c: HonoContext) => string | undefined\n\n /** Default queue priority, or derive it per-request. */\n priority?: Priority | ((c: HonoContext) => Priority)\n\n /** Inject X-RateLimit-* headers. Pass model ID or function. */\n injectHeaders?: string | ((c: HonoContext) => string)\n}\n\n/**\n * Hono middleware that attaches rateLimiter context and catches RateLimiterErrors.\n *\n * Access the context in route handlers via `c.var.rateLimiter`.\n */\nexport function createHonoMiddleware(\n limiter: RateLimiter,\n options: HonoMiddlewareOptions = {},\n): HonoMiddlewareHandler {\n return async (c, next) => {\n const scope = options.scope?.(c)\n const priority = typeof options.priority === 'function'\n ? options.priority(c)\n : options.priority\n\n const ctx: RateLimiterRequestContext = {\n ...(scope !== undefined && { scope }),\n ...(priority !== undefined && { priority }),\n }\n c.set('rateLimiter', ctx)\n\n if (options.injectHeaders) {\n const modelId = typeof options.injectHeaders === 'function'\n ? options.injectHeaders(c)\n : options.injectHeaders\n const status = limiter.getStatus()\n const modelStat = status.models.find(m => m.modelId === modelId)\n\n if (modelStat) {\n c.header('X-RateLimit-Model', modelId)\n c.header('X-RateLimit-Queue-Depth', String(modelStat.queueDepth))\n c.header('X-RateLimit-Requests-Window', String(modelStat.requestsInWindow))\n if (modelStat.estimatedWaitMs > 0) {\n c.header('X-RateLimit-Estimated-Wait-Ms', String(modelStat.estimatedWaitMs))\n }\n }\n }\n\n try {\n await next()\n } catch (err) {\n if (err instanceof RateLimiterError) {\n const { status, body } = mapErrorToResponse(err)\n return c.json(body, status as Parameters<typeof c.json>[1])\n }\n throw err\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared: error → HTTP response\n// ---------------------------------------------------------------------------\n\n/**\n * Map any RateLimiterError to an HTTP status code + JSON body.\n *\n * Exported so you can use it in custom error handlers, non-Express frameworks,\n * or API gateway integrations.\n *\n * @example\n * ```typescript\n * import { mapErrorToResponse } from 'ai-sdk-rate-limiter/middleware'\n *\n * // Fastify onError hook\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof RateLimiterError) {\n * const { status, body } = mapErrorToResponse(err)\n * return reply.status(status).send(body)\n * }\n * reply.send(err)\n * })\n * ```\n */\nexport function mapErrorToResponse(\n err: RateLimiterError,\n includeDetails = true,\n): { status: number; body: Record<string, unknown> } {\n if (err instanceof QueueTimeoutError) {\n return {\n status: 503,\n body: {\n error: 'Request queued too long. Try again shortly.',\n code: 'QUEUE_TIMEOUT',\n ...(includeDetails && {\n retryAfterMs: 5_000,\n queueDepth: err.queueDepth,\n }),\n },\n }\n }\n\n if (err instanceof QueueFullError) {\n return {\n status: 503,\n body: {\n error: 'Server is busy. Try again in a moment.',\n code: 'QUEUE_FULL',\n },\n }\n }\n\n if (err instanceof BudgetExceededError) {\n return {\n status: 402,\n body: {\n error: 'AI usage budget exceeded.',\n code: 'BUDGET_EXCEEDED',\n ...(includeDetails && {\n period: err.period,\n limitUsd: err.limitUsd,\n currentCostUsd: err.currentCostUsd,\n }),\n },\n }\n }\n\n if (err instanceof CircuitOpenError) {\n return {\n status: 503,\n body: {\n error: 'AI provider temporarily unavailable.',\n code: 'CIRCUIT_OPEN',\n ...(includeDetails && {\n retryAfter: Math.max(0, Math.ceil((err.openUntilMs - Date.now()) / 1000)),\n }),\n },\n }\n }\n\n if (err instanceof ShutdownError) {\n return {\n status: 503,\n body: {\n error: 'Service is shutting down.',\n code: 'SHUTDOWN',\n },\n }\n }\n\n return {\n status: 429,\n body: {\n error: 'Rate limit exceeded.',\n code: 'RATE_LIMITED',\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { P as Priority, a as RateLimiter } from './types-CUPpMRPE.cjs';
|
|
2
|
+
import { b as RateLimiterError } from './errors-DcXM0HCM.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ai-sdk-rate-limiter/middleware
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic middleware helpers. Reduces per-route boilerplate to zero:
|
|
8
|
+
* scope extraction, priority assignment, and rate-limiter error handling are
|
|
9
|
+
* all handled at the middleware layer.
|
|
10
|
+
*
|
|
11
|
+
* @example Express
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createRateLimiterMiddleware } from 'ai-sdk-rate-limiter/middleware'
|
|
14
|
+
*
|
|
15
|
+
* const { middleware, errorHandler } = createRateLimiterMiddleware(limiter, {
|
|
16
|
+
* scope: (req) => `user:${req.headers['x-user-id']}`,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* app.use(middleware) // BEFORE routes — attaches req.rateLimiter
|
|
20
|
+
*
|
|
21
|
+
* app.post('/chat', async (req, res) => {
|
|
22
|
+
* await generateText({
|
|
23
|
+
* model,
|
|
24
|
+
* providerOptions: { rateLimiter: req.rateLimiter }, // just pass it through
|
|
25
|
+
* })
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* app.use(errorHandler) // AFTER routes — converts errors to proper HTTP responses
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example Hono
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { createHonoMiddleware } from 'ai-sdk-rate-limiter/middleware'
|
|
34
|
+
*
|
|
35
|
+
* app.use(createHonoMiddleware(limiter, {
|
|
36
|
+
* scope: (c) => c.req.header('x-user-id'),
|
|
37
|
+
* }))
|
|
38
|
+
*
|
|
39
|
+
* app.post('/chat', async (c) => {
|
|
40
|
+
* await generateText({
|
|
41
|
+
* model,
|
|
42
|
+
* providerOptions: { rateLimiter: c.var.rateLimiter },
|
|
43
|
+
* })
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
interface RateLimiterRequestContext {
|
|
49
|
+
/** Scope key for per-user/org isolated rate limiting */
|
|
50
|
+
scope?: string;
|
|
51
|
+
/** Queue priority for this request. Default: 'normal' */
|
|
52
|
+
priority?: Priority;
|
|
53
|
+
}
|
|
54
|
+
declare module 'http' {
|
|
55
|
+
interface IncomingMessage {
|
|
56
|
+
/**
|
|
57
|
+
* Populated by createRateLimiterMiddleware(). Pass directly to providerOptions:
|
|
58
|
+
* ```typescript
|
|
59
|
+
* providerOptions: { rateLimiter: req.rateLimiter }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
rateLimiter?: RateLimiterRequestContext;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
interface MinReq {
|
|
66
|
+
headers: Record<string, string | string[] | undefined>;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
interface MinRes {
|
|
70
|
+
setHeader(name: string, value: string | number): void;
|
|
71
|
+
status(code: number): MinRes;
|
|
72
|
+
json(body: unknown): void;
|
|
73
|
+
readonly headersSent: boolean;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
type NextFn = (err?: unknown) => void;
|
|
77
|
+
interface RateLimiterMiddlewareOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Extract the per-request scope from the incoming request.
|
|
80
|
+
* Stored in req.rateLimiter.scope.
|
|
81
|
+
*
|
|
82
|
+
* @example (req) => req.headers['x-user-id'] as string
|
|
83
|
+
* @example (req) => `user:${(req as any).user.id}`
|
|
84
|
+
*/
|
|
85
|
+
scope?: (req: MinReq) => string | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Default queue priority, or derive it per-request.
|
|
88
|
+
* Stored in req.rateLimiter.priority. Default: 'normal'
|
|
89
|
+
*
|
|
90
|
+
* @example (req) => req.headers['x-priority'] === 'high' ? 'high' : 'normal'
|
|
91
|
+
*/
|
|
92
|
+
priority?: Priority | ((req: MinReq) => Priority);
|
|
93
|
+
/**
|
|
94
|
+
* Inject X-RateLimit-* informational headers into every response.
|
|
95
|
+
* Pass the model ID to inspect, or a function to derive it per-request.
|
|
96
|
+
*
|
|
97
|
+
* @example 'gpt-4o'
|
|
98
|
+
* @example (req) => req.headers['x-ai-model'] as string ?? 'gpt-4o-mini'
|
|
99
|
+
*/
|
|
100
|
+
injectHeaders?: string | ((req: MinReq) => string);
|
|
101
|
+
}
|
|
102
|
+
interface ErrorHandlerOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Include structured details (retryAfter, period, limitUsd…) in the
|
|
105
|
+
* response body. Default: true
|
|
106
|
+
*/
|
|
107
|
+
includeDetails?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Override the default error → HTTP mapping.
|
|
110
|
+
* Return null/undefined to fall through to the next error handler.
|
|
111
|
+
*/
|
|
112
|
+
format?: (err: RateLimiterError) => {
|
|
113
|
+
status: number;
|
|
114
|
+
body: unknown;
|
|
115
|
+
} | null | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Returns a middleware + error handler pair for Express (or any Node.js
|
|
119
|
+
* framework that uses the `(req, res, next)` calling convention).
|
|
120
|
+
*
|
|
121
|
+
* **middleware** — place BEFORE routes. Attaches req.rateLimiter.
|
|
122
|
+
* **errorHandler** — place AFTER routes. Converts RateLimiterErrors to HTTP.
|
|
123
|
+
*/
|
|
124
|
+
declare function createRateLimiterMiddleware(limiter: RateLimiter, options?: RateLimiterMiddlewareOptions): {
|
|
125
|
+
middleware: (req: MinReq, res: MinRes, next: NextFn) => void;
|
|
126
|
+
errorHandler: (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Standalone Express 4-argument error handler.
|
|
130
|
+
* Use this when you only need error handling and not scope injection.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* app.use(createRateLimiterErrorHandler({ includeDetails: false }))
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
declare function createRateLimiterErrorHandler(options?: ErrorHandlerOptions): (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void;
|
|
138
|
+
/**
|
|
139
|
+
* Minimal Hono Context interface — structural typing, no hard `hono` dep.
|
|
140
|
+
*/
|
|
141
|
+
interface HonoContext {
|
|
142
|
+
req: {
|
|
143
|
+
raw: Request;
|
|
144
|
+
header(name: string): string | undefined;
|
|
145
|
+
};
|
|
146
|
+
set(key: string, value: unknown): void;
|
|
147
|
+
json(body: unknown, status?: number): Response;
|
|
148
|
+
header(name: string, value: string): void;
|
|
149
|
+
var: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
type HonoNext = () => Promise<Response | void>;
|
|
152
|
+
/** Hono middleware handler signature */
|
|
153
|
+
type HonoMiddlewareHandler = (c: HonoContext, next: HonoNext) => Promise<Response | void>;
|
|
154
|
+
interface HonoMiddlewareOptions {
|
|
155
|
+
/**
|
|
156
|
+
* Extract scope from the Hono context. Stored in c.var.rateLimiter.scope.
|
|
157
|
+
*
|
|
158
|
+
* @example (c) => c.req.header('x-user-id')
|
|
159
|
+
* @example (c) => c.var.user?.id ? `user:${c.var.user.id}` : undefined
|
|
160
|
+
*/
|
|
161
|
+
scope?: (c: HonoContext) => string | undefined;
|
|
162
|
+
/** Default queue priority, or derive it per-request. */
|
|
163
|
+
priority?: Priority | ((c: HonoContext) => Priority);
|
|
164
|
+
/** Inject X-RateLimit-* headers. Pass model ID or function. */
|
|
165
|
+
injectHeaders?: string | ((c: HonoContext) => string);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Hono middleware that attaches rateLimiter context and catches RateLimiterErrors.
|
|
169
|
+
*
|
|
170
|
+
* Access the context in route handlers via `c.var.rateLimiter`.
|
|
171
|
+
*/
|
|
172
|
+
declare function createHonoMiddleware(limiter: RateLimiter, options?: HonoMiddlewareOptions): HonoMiddlewareHandler;
|
|
173
|
+
/**
|
|
174
|
+
* Map any RateLimiterError to an HTTP status code + JSON body.
|
|
175
|
+
*
|
|
176
|
+
* Exported so you can use it in custom error handlers, non-Express frameworks,
|
|
177
|
+
* or API gateway integrations.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* import { mapErrorToResponse } from 'ai-sdk-rate-limiter/middleware'
|
|
182
|
+
*
|
|
183
|
+
* // Fastify onError hook
|
|
184
|
+
* fastify.setErrorHandler((err, request, reply) => {
|
|
185
|
+
* if (err instanceof RateLimiterError) {
|
|
186
|
+
* const { status, body } = mapErrorToResponse(err)
|
|
187
|
+
* return reply.status(status).send(body)
|
|
188
|
+
* }
|
|
189
|
+
* reply.send(err)
|
|
190
|
+
* })
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
declare function mapErrorToResponse(err: RateLimiterError, includeDetails?: boolean): {
|
|
194
|
+
status: number;
|
|
195
|
+
body: Record<string, unknown>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export { type ErrorHandlerOptions, type HonoContext, type HonoMiddlewareHandler, type HonoMiddlewareOptions, type RateLimiterMiddlewareOptions, type RateLimiterRequestContext, createHonoMiddleware, createRateLimiterErrorHandler, createRateLimiterMiddleware, mapErrorToResponse };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { P as Priority, a as RateLimiter } from './types-CUPpMRPE.js';
|
|
2
|
+
import { b as RateLimiterError } from './errors-DcXM0HCM.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ai-sdk-rate-limiter/middleware
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic middleware helpers. Reduces per-route boilerplate to zero:
|
|
8
|
+
* scope extraction, priority assignment, and rate-limiter error handling are
|
|
9
|
+
* all handled at the middleware layer.
|
|
10
|
+
*
|
|
11
|
+
* @example Express
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createRateLimiterMiddleware } from 'ai-sdk-rate-limiter/middleware'
|
|
14
|
+
*
|
|
15
|
+
* const { middleware, errorHandler } = createRateLimiterMiddleware(limiter, {
|
|
16
|
+
* scope: (req) => `user:${req.headers['x-user-id']}`,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* app.use(middleware) // BEFORE routes — attaches req.rateLimiter
|
|
20
|
+
*
|
|
21
|
+
* app.post('/chat', async (req, res) => {
|
|
22
|
+
* await generateText({
|
|
23
|
+
* model,
|
|
24
|
+
* providerOptions: { rateLimiter: req.rateLimiter }, // just pass it through
|
|
25
|
+
* })
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* app.use(errorHandler) // AFTER routes — converts errors to proper HTTP responses
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example Hono
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { createHonoMiddleware } from 'ai-sdk-rate-limiter/middleware'
|
|
34
|
+
*
|
|
35
|
+
* app.use(createHonoMiddleware(limiter, {
|
|
36
|
+
* scope: (c) => c.req.header('x-user-id'),
|
|
37
|
+
* }))
|
|
38
|
+
*
|
|
39
|
+
* app.post('/chat', async (c) => {
|
|
40
|
+
* await generateText({
|
|
41
|
+
* model,
|
|
42
|
+
* providerOptions: { rateLimiter: c.var.rateLimiter },
|
|
43
|
+
* })
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
interface RateLimiterRequestContext {
|
|
49
|
+
/** Scope key for per-user/org isolated rate limiting */
|
|
50
|
+
scope?: string;
|
|
51
|
+
/** Queue priority for this request. Default: 'normal' */
|
|
52
|
+
priority?: Priority;
|
|
53
|
+
}
|
|
54
|
+
declare module 'http' {
|
|
55
|
+
interface IncomingMessage {
|
|
56
|
+
/**
|
|
57
|
+
* Populated by createRateLimiterMiddleware(). Pass directly to providerOptions:
|
|
58
|
+
* ```typescript
|
|
59
|
+
* providerOptions: { rateLimiter: req.rateLimiter }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
rateLimiter?: RateLimiterRequestContext;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
interface MinReq {
|
|
66
|
+
headers: Record<string, string | string[] | undefined>;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
interface MinRes {
|
|
70
|
+
setHeader(name: string, value: string | number): void;
|
|
71
|
+
status(code: number): MinRes;
|
|
72
|
+
json(body: unknown): void;
|
|
73
|
+
readonly headersSent: boolean;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
type NextFn = (err?: unknown) => void;
|
|
77
|
+
interface RateLimiterMiddlewareOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Extract the per-request scope from the incoming request.
|
|
80
|
+
* Stored in req.rateLimiter.scope.
|
|
81
|
+
*
|
|
82
|
+
* @example (req) => req.headers['x-user-id'] as string
|
|
83
|
+
* @example (req) => `user:${(req as any).user.id}`
|
|
84
|
+
*/
|
|
85
|
+
scope?: (req: MinReq) => string | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Default queue priority, or derive it per-request.
|
|
88
|
+
* Stored in req.rateLimiter.priority. Default: 'normal'
|
|
89
|
+
*
|
|
90
|
+
* @example (req) => req.headers['x-priority'] === 'high' ? 'high' : 'normal'
|
|
91
|
+
*/
|
|
92
|
+
priority?: Priority | ((req: MinReq) => Priority);
|
|
93
|
+
/**
|
|
94
|
+
* Inject X-RateLimit-* informational headers into every response.
|
|
95
|
+
* Pass the model ID to inspect, or a function to derive it per-request.
|
|
96
|
+
*
|
|
97
|
+
* @example 'gpt-4o'
|
|
98
|
+
* @example (req) => req.headers['x-ai-model'] as string ?? 'gpt-4o-mini'
|
|
99
|
+
*/
|
|
100
|
+
injectHeaders?: string | ((req: MinReq) => string);
|
|
101
|
+
}
|
|
102
|
+
interface ErrorHandlerOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Include structured details (retryAfter, period, limitUsd…) in the
|
|
105
|
+
* response body. Default: true
|
|
106
|
+
*/
|
|
107
|
+
includeDetails?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Override the default error → HTTP mapping.
|
|
110
|
+
* Return null/undefined to fall through to the next error handler.
|
|
111
|
+
*/
|
|
112
|
+
format?: (err: RateLimiterError) => {
|
|
113
|
+
status: number;
|
|
114
|
+
body: unknown;
|
|
115
|
+
} | null | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Returns a middleware + error handler pair for Express (or any Node.js
|
|
119
|
+
* framework that uses the `(req, res, next)` calling convention).
|
|
120
|
+
*
|
|
121
|
+
* **middleware** — place BEFORE routes. Attaches req.rateLimiter.
|
|
122
|
+
* **errorHandler** — place AFTER routes. Converts RateLimiterErrors to HTTP.
|
|
123
|
+
*/
|
|
124
|
+
declare function createRateLimiterMiddleware(limiter: RateLimiter, options?: RateLimiterMiddlewareOptions): {
|
|
125
|
+
middleware: (req: MinReq, res: MinRes, next: NextFn) => void;
|
|
126
|
+
errorHandler: (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Standalone Express 4-argument error handler.
|
|
130
|
+
* Use this when you only need error handling and not scope injection.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* app.use(createRateLimiterErrorHandler({ includeDetails: false }))
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
declare function createRateLimiterErrorHandler(options?: ErrorHandlerOptions): (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void;
|
|
138
|
+
/**
|
|
139
|
+
* Minimal Hono Context interface — structural typing, no hard `hono` dep.
|
|
140
|
+
*/
|
|
141
|
+
interface HonoContext {
|
|
142
|
+
req: {
|
|
143
|
+
raw: Request;
|
|
144
|
+
header(name: string): string | undefined;
|
|
145
|
+
};
|
|
146
|
+
set(key: string, value: unknown): void;
|
|
147
|
+
json(body: unknown, status?: number): Response;
|
|
148
|
+
header(name: string, value: string): void;
|
|
149
|
+
var: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
type HonoNext = () => Promise<Response | void>;
|
|
152
|
+
/** Hono middleware handler signature */
|
|
153
|
+
type HonoMiddlewareHandler = (c: HonoContext, next: HonoNext) => Promise<Response | void>;
|
|
154
|
+
interface HonoMiddlewareOptions {
|
|
155
|
+
/**
|
|
156
|
+
* Extract scope from the Hono context. Stored in c.var.rateLimiter.scope.
|
|
157
|
+
*
|
|
158
|
+
* @example (c) => c.req.header('x-user-id')
|
|
159
|
+
* @example (c) => c.var.user?.id ? `user:${c.var.user.id}` : undefined
|
|
160
|
+
*/
|
|
161
|
+
scope?: (c: HonoContext) => string | undefined;
|
|
162
|
+
/** Default queue priority, or derive it per-request. */
|
|
163
|
+
priority?: Priority | ((c: HonoContext) => Priority);
|
|
164
|
+
/** Inject X-RateLimit-* headers. Pass model ID or function. */
|
|
165
|
+
injectHeaders?: string | ((c: HonoContext) => string);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Hono middleware that attaches rateLimiter context and catches RateLimiterErrors.
|
|
169
|
+
*
|
|
170
|
+
* Access the context in route handlers via `c.var.rateLimiter`.
|
|
171
|
+
*/
|
|
172
|
+
declare function createHonoMiddleware(limiter: RateLimiter, options?: HonoMiddlewareOptions): HonoMiddlewareHandler;
|
|
173
|
+
/**
|
|
174
|
+
* Map any RateLimiterError to an HTTP status code + JSON body.
|
|
175
|
+
*
|
|
176
|
+
* Exported so you can use it in custom error handlers, non-Express frameworks,
|
|
177
|
+
* or API gateway integrations.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* import { mapErrorToResponse } from 'ai-sdk-rate-limiter/middleware'
|
|
182
|
+
*
|
|
183
|
+
* // Fastify onError hook
|
|
184
|
+
* fastify.setErrorHandler((err, request, reply) => {
|
|
185
|
+
* if (err instanceof RateLimiterError) {
|
|
186
|
+
* const { status, body } = mapErrorToResponse(err)
|
|
187
|
+
* return reply.status(status).send(body)
|
|
188
|
+
* }
|
|
189
|
+
* reply.send(err)
|
|
190
|
+
* })
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
declare function mapErrorToResponse(err: RateLimiterError, includeDetails?: boolean): {
|
|
194
|
+
status: number;
|
|
195
|
+
body: Record<string, unknown>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export { type ErrorHandlerOptions, type HonoContext, type HonoMiddlewareHandler, type HonoMiddlewareOptions, type RateLimiterMiddlewareOptions, type RateLimiterRequestContext, createHonoMiddleware, createRateLimiterErrorHandler, createRateLimiterMiddleware, mapErrorToResponse };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var RateLimiterError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "RateLimiterError";
|
|
6
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var QueueTimeoutError = class extends RateLimiterError {
|
|
10
|
+
constructor(model, waitedMs, queueDepth) {
|
|
11
|
+
super(
|
|
12
|
+
`Request for model "${model}" timed out after waiting ${waitedMs}ms in the queue (current queue depth: ${queueDepth}).`
|
|
13
|
+
);
|
|
14
|
+
this.model = model;
|
|
15
|
+
this.waitedMs = waitedMs;
|
|
16
|
+
this.queueDepth = queueDepth;
|
|
17
|
+
this.name = "QueueTimeoutError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var QueueFullError = class extends RateLimiterError {
|
|
21
|
+
constructor(model, maxSize) {
|
|
22
|
+
super(
|
|
23
|
+
`Queue for model "${model}" is full (maxSize: ${maxSize}). Increase queue.maxSize or reduce request rate.`
|
|
24
|
+
);
|
|
25
|
+
this.model = model;
|
|
26
|
+
this.maxSize = maxSize;
|
|
27
|
+
this.name = "QueueFullError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var BudgetExceededError = class extends RateLimiterError {
|
|
31
|
+
constructor(model, currentCostUsd, limitUsd, period) {
|
|
32
|
+
super(
|
|
33
|
+
`Cost budget exceeded for model "${model}": $${currentCostUsd.toFixed(4)} used of $${limitUsd.toFixed(2)} ${period} budget.`
|
|
34
|
+
);
|
|
35
|
+
this.model = model;
|
|
36
|
+
this.currentCostUsd = currentCostUsd;
|
|
37
|
+
this.limitUsd = limitUsd;
|
|
38
|
+
this.period = period;
|
|
39
|
+
this.name = "BudgetExceededError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var CircuitOpenError = class extends RateLimiterError {
|
|
43
|
+
constructor(model, openUntilMs) {
|
|
44
|
+
super(
|
|
45
|
+
`Circuit breaker for model "${model}" is open due to repeated failures. Requests are blocked until ${new Date(openUntilMs).toISOString()}.`
|
|
46
|
+
);
|
|
47
|
+
this.model = model;
|
|
48
|
+
this.openUntilMs = openUntilMs;
|
|
49
|
+
this.name = "CircuitOpenError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ShutdownError = class extends RateLimiterError {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("Rate limiter is shutting down \u2014 new requests are not accepted.");
|
|
55
|
+
this.name = "ShutdownError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/middleware.ts
|
|
60
|
+
function createRateLimiterMiddleware(limiter, options = {}) {
|
|
61
|
+
const middleware = (req, res, next) => {
|
|
62
|
+
const scope = options.scope?.(req);
|
|
63
|
+
const priority = typeof options.priority === "function" ? options.priority(req) : options.priority;
|
|
64
|
+
const ctx = {
|
|
65
|
+
...scope !== void 0 && { scope },
|
|
66
|
+
...priority !== void 0 && { priority }
|
|
67
|
+
};
|
|
68
|
+
req["rateLimiter"] = ctx;
|
|
69
|
+
if (options.injectHeaders && !res.headersSent) {
|
|
70
|
+
const modelId = typeof options.injectHeaders === "function" ? options.injectHeaders(req) : options.injectHeaders;
|
|
71
|
+
const status = limiter.getStatus();
|
|
72
|
+
const modelStat = status.models.find((m) => m.modelId === modelId);
|
|
73
|
+
if (modelStat) {
|
|
74
|
+
res.setHeader("X-RateLimit-Model", modelId);
|
|
75
|
+
res.setHeader("X-RateLimit-Queue-Depth", modelStat.queueDepth);
|
|
76
|
+
res.setHeader("X-RateLimit-Requests-Window", modelStat.requestsInWindow);
|
|
77
|
+
if (modelStat.estimatedWaitMs > 0) {
|
|
78
|
+
res.setHeader("X-RateLimit-Estimated-Wait-Ms", modelStat.estimatedWaitMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
next();
|
|
83
|
+
};
|
|
84
|
+
const errorHandler = (err, _req, res, next) => {
|
|
85
|
+
if (!(err instanceof RateLimiterError)) {
|
|
86
|
+
next(err);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (res.headersSent) {
|
|
90
|
+
next(err);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { status, body } = mapErrorToResponse(err);
|
|
94
|
+
res.status(status).json(body);
|
|
95
|
+
};
|
|
96
|
+
return { middleware, errorHandler };
|
|
97
|
+
}
|
|
98
|
+
function createRateLimiterErrorHandler(options = {}) {
|
|
99
|
+
return (err, _req, res, next) => {
|
|
100
|
+
if (!(err instanceof RateLimiterError)) {
|
|
101
|
+
next(err);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (res.headersSent) {
|
|
105
|
+
next(err);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (options.format) {
|
|
109
|
+
const custom = options.format(err);
|
|
110
|
+
if (custom == null) {
|
|
111
|
+
next(err);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
res.status(custom.status).json(custom.body);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { status, body } = mapErrorToResponse(err, options.includeDetails);
|
|
118
|
+
res.status(status).json(body);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function createHonoMiddleware(limiter, options = {}) {
|
|
122
|
+
return async (c, next) => {
|
|
123
|
+
const scope = options.scope?.(c);
|
|
124
|
+
const priority = typeof options.priority === "function" ? options.priority(c) : options.priority;
|
|
125
|
+
const ctx = {
|
|
126
|
+
...scope !== void 0 && { scope },
|
|
127
|
+
...priority !== void 0 && { priority }
|
|
128
|
+
};
|
|
129
|
+
c.set("rateLimiter", ctx);
|
|
130
|
+
if (options.injectHeaders) {
|
|
131
|
+
const modelId = typeof options.injectHeaders === "function" ? options.injectHeaders(c) : options.injectHeaders;
|
|
132
|
+
const status = limiter.getStatus();
|
|
133
|
+
const modelStat = status.models.find((m) => m.modelId === modelId);
|
|
134
|
+
if (modelStat) {
|
|
135
|
+
c.header("X-RateLimit-Model", modelId);
|
|
136
|
+
c.header("X-RateLimit-Queue-Depth", String(modelStat.queueDepth));
|
|
137
|
+
c.header("X-RateLimit-Requests-Window", String(modelStat.requestsInWindow));
|
|
138
|
+
if (modelStat.estimatedWaitMs > 0) {
|
|
139
|
+
c.header("X-RateLimit-Estimated-Wait-Ms", String(modelStat.estimatedWaitMs));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await next();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof RateLimiterError) {
|
|
147
|
+
const { status, body } = mapErrorToResponse(err);
|
|
148
|
+
return c.json(body, status);
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function mapErrorToResponse(err, includeDetails = true) {
|
|
155
|
+
if (err instanceof QueueTimeoutError) {
|
|
156
|
+
return {
|
|
157
|
+
status: 503,
|
|
158
|
+
body: {
|
|
159
|
+
error: "Request queued too long. Try again shortly.",
|
|
160
|
+
code: "QUEUE_TIMEOUT",
|
|
161
|
+
...includeDetails && {
|
|
162
|
+
retryAfterMs: 5e3,
|
|
163
|
+
queueDepth: err.queueDepth
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (err instanceof QueueFullError) {
|
|
169
|
+
return {
|
|
170
|
+
status: 503,
|
|
171
|
+
body: {
|
|
172
|
+
error: "Server is busy. Try again in a moment.",
|
|
173
|
+
code: "QUEUE_FULL"
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (err instanceof BudgetExceededError) {
|
|
178
|
+
return {
|
|
179
|
+
status: 402,
|
|
180
|
+
body: {
|
|
181
|
+
error: "AI usage budget exceeded.",
|
|
182
|
+
code: "BUDGET_EXCEEDED",
|
|
183
|
+
...includeDetails && {
|
|
184
|
+
period: err.period,
|
|
185
|
+
limitUsd: err.limitUsd,
|
|
186
|
+
currentCostUsd: err.currentCostUsd
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (err instanceof CircuitOpenError) {
|
|
192
|
+
return {
|
|
193
|
+
status: 503,
|
|
194
|
+
body: {
|
|
195
|
+
error: "AI provider temporarily unavailable.",
|
|
196
|
+
code: "CIRCUIT_OPEN",
|
|
197
|
+
...includeDetails && {
|
|
198
|
+
retryAfter: Math.max(0, Math.ceil((err.openUntilMs - Date.now()) / 1e3))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (err instanceof ShutdownError) {
|
|
204
|
+
return {
|
|
205
|
+
status: 503,
|
|
206
|
+
body: {
|
|
207
|
+
error: "Service is shutting down.",
|
|
208
|
+
code: "SHUTDOWN"
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
status: 429,
|
|
214
|
+
body: {
|
|
215
|
+
error: "Rate limit exceeded.",
|
|
216
|
+
code: "RATE_LIMITED"
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { createHonoMiddleware, createRateLimiterErrorHandler, createRateLimiterMiddleware, mapErrorToResponse };
|
|
222
|
+
//# sourceMappingURL=middleware.js.map
|
|
223
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/middleware.ts"],"names":[],"mappings":"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","file":"middleware.js","sourcesContent":["/** Base class for all ai-sdk-rate-limiter errors */\nexport class RateLimiterError extends Error {\n // Declared as mutable string so subclasses can assign in constructors\n declare name: string\n\n constructor(message: string) {\n super(message)\n this.name = 'RateLimiterError'\n // Restore prototype chain (needed when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/**\n * Thrown when a request cannot proceed because the rate limit was hit\n * and the request either timed out waiting in the queue or exhausted all retries.\n */\nexport class RateLimitExceededError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly limitType: 'rpm' | 'itpm' | 'otpm',\n public readonly limit: number,\n public readonly resetAt: number,\n ) {\n super(\n `Rate limit exceeded for model \"${model}\": ${limitType.toUpperCase()} limit of ${limit} hit. ` +\n `Resets at ${new Date(resetAt).toISOString()}.`,\n )\n this.name = 'RateLimitExceededError'\n }\n}\n\n/**\n * Thrown when a request has waited in the queue longer than the configured timeout.\n */\nexport class QueueTimeoutError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly waitedMs: number,\n public readonly queueDepth: number,\n ) {\n super(\n `Request for model \"${model}\" timed out after waiting ${waitedMs}ms in the queue ` +\n `(current queue depth: ${queueDepth}).`,\n )\n this.name = 'QueueTimeoutError'\n }\n}\n\n/**\n * Thrown when a new request arrives and the queue is at capacity.\n */\nexport class QueueFullError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly maxSize: number,\n ) {\n super(\n `Queue for model \"${model}\" is full (maxSize: ${maxSize}). ` +\n `Increase queue.maxSize or reduce request rate.`,\n )\n this.name = 'QueueFullError'\n }\n}\n\n/**\n * Thrown when a request would exceed the configured cost budget.\n */\nexport class BudgetExceededError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly currentCostUsd: number,\n public readonly limitUsd: number,\n public readonly period: 'hourly' | 'daily' | 'monthly',\n ) {\n super(\n `Cost budget exceeded for model \"${model}\": ` +\n `$${currentCostUsd.toFixed(4)} used of $${limitUsd.toFixed(2)} ${period} budget.`,\n )\n this.name = 'BudgetExceededError'\n }\n}\n\n/**\n * Thrown when a request is blocked because the circuit breaker is open.\n */\nexport class CircuitOpenError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly openUntilMs: number,\n ) {\n super(\n `Circuit breaker for model \"${model}\" is open due to repeated failures. ` +\n `Requests are blocked until ${new Date(openUntilMs).toISOString()}.`,\n )\n this.name = 'CircuitOpenError'\n }\n}\n\n/**\n * Thrown when a request arrives after shutdown() has been called.\n */\nexport class ShutdownError extends RateLimiterError {\n constructor() {\n super('Rate limiter is shutting down — new requests are not accepted.')\n this.name = 'ShutdownError'\n }\n}\n\n/**\n * Thrown when all retry attempts are exhausted.\n */\nexport class RetryExhaustedError extends RateLimiterError {\n constructor(\n public readonly model: string,\n public readonly attempts: number,\n public readonly cause: unknown,\n ) {\n super(\n `All ${attempts} retry attempts exhausted for model \"${model}\". ` +\n `Last error: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n this.name = 'RetryExhaustedError'\n if (cause instanceof Error) {\n this.stack = `${this.stack}\\nCaused by: ${cause.stack}`\n }\n }\n}\n","/**\n * ai-sdk-rate-limiter/middleware\n *\n * Framework-agnostic middleware helpers. Reduces per-route boilerplate to zero:\n * scope extraction, priority assignment, and rate-limiter error handling are\n * all handled at the middleware layer.\n *\n * @example Express\n * ```typescript\n * import { createRateLimiterMiddleware } from 'ai-sdk-rate-limiter/middleware'\n *\n * const { middleware, errorHandler } = createRateLimiterMiddleware(limiter, {\n * scope: (req) => `user:${req.headers['x-user-id']}`,\n * })\n *\n * app.use(middleware) // BEFORE routes — attaches req.rateLimiter\n *\n * app.post('/chat', async (req, res) => {\n * await generateText({\n * model,\n * providerOptions: { rateLimiter: req.rateLimiter }, // just pass it through\n * })\n * })\n *\n * app.use(errorHandler) // AFTER routes — converts errors to proper HTTP responses\n * ```\n *\n * @example Hono\n * ```typescript\n * import { createHonoMiddleware } from 'ai-sdk-rate-limiter/middleware'\n *\n * app.use(createHonoMiddleware(limiter, {\n * scope: (c) => c.req.header('x-user-id'),\n * }))\n *\n * app.post('/chat', async (c) => {\n * await generateText({\n * model,\n * providerOptions: { rateLimiter: c.var.rateLimiter },\n * })\n * })\n * ```\n */\n\nimport type { RateLimiter, Priority } from './types.js'\nimport {\n RateLimiterError,\n QueueTimeoutError,\n QueueFullError,\n BudgetExceededError,\n CircuitOpenError,\n ShutdownError,\n} from './errors.js'\n\n// ---------------------------------------------------------------------------\n// Shared request context\n//\n// Stored on req.rateLimiter (Express) or c.var.rateLimiter (Hono).\n// Pass directly to providerOptions.rateLimiter in route handlers.\n// ---------------------------------------------------------------------------\n\nexport interface RateLimiterRequestContext {\n /** Scope key for per-user/org isolated rate limiting */\n scope?: string\n /** Queue priority for this request. Default: 'normal' */\n priority?: Priority\n}\n\n// Augment Node.js http.IncomingMessage so TypeScript knows about req.rateLimiter\n// without requiring users to install @types/express separately.\ndeclare module 'http' {\n interface IncomingMessage {\n /**\n * Populated by createRateLimiterMiddleware(). Pass directly to providerOptions:\n * ```typescript\n * providerOptions: { rateLimiter: req.rateLimiter }\n * ```\n */\n rateLimiter?: RateLimiterRequestContext\n }\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types — no runtime dep on express / hono / fastify\n// ---------------------------------------------------------------------------\n\ninterface MinReq {\n headers: Record<string, string | string[] | undefined>\n [key: string]: unknown\n}\n\ninterface MinRes {\n setHeader(name: string, value: string | number): void\n status(code: number): MinRes\n json(body: unknown): void\n readonly headersSent: boolean\n [key: string]: unknown\n}\n\ntype NextFn = (err?: unknown) => void\n\n// ---------------------------------------------------------------------------\n// Options — Express\n// ---------------------------------------------------------------------------\n\nexport interface RateLimiterMiddlewareOptions {\n /**\n * Extract the per-request scope from the incoming request.\n * Stored in req.rateLimiter.scope.\n *\n * @example (req) => req.headers['x-user-id'] as string\n * @example (req) => `user:${(req as any).user.id}`\n */\n scope?: (req: MinReq) => string | undefined\n\n /**\n * Default queue priority, or derive it per-request.\n * Stored in req.rateLimiter.priority. Default: 'normal'\n *\n * @example (req) => req.headers['x-priority'] === 'high' ? 'high' : 'normal'\n */\n priority?: Priority | ((req: MinReq) => Priority)\n\n /**\n * Inject X-RateLimit-* informational headers into every response.\n * Pass the model ID to inspect, or a function to derive it per-request.\n *\n * @example 'gpt-4o'\n * @example (req) => req.headers['x-ai-model'] as string ?? 'gpt-4o-mini'\n */\n injectHeaders?: string | ((req: MinReq) => string)\n}\n\nexport interface ErrorHandlerOptions {\n /**\n * Include structured details (retryAfter, period, limitUsd…) in the\n * response body. Default: true\n */\n includeDetails?: boolean\n\n /**\n * Override the default error → HTTP mapping.\n * Return null/undefined to fall through to the next error handler.\n */\n format?: (err: RateLimiterError) => { status: number; body: unknown } | null | undefined\n}\n\n// ---------------------------------------------------------------------------\n// Express: createRateLimiterMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a middleware + error handler pair for Express (or any Node.js\n * framework that uses the `(req, res, next)` calling convention).\n *\n * **middleware** — place BEFORE routes. Attaches req.rateLimiter.\n * **errorHandler** — place AFTER routes. Converts RateLimiterErrors to HTTP.\n */\nexport function createRateLimiterMiddleware(\n limiter: RateLimiter,\n options: RateLimiterMiddlewareOptions = {},\n): {\n middleware: (req: MinReq, res: MinRes, next: NextFn) => void\n errorHandler: (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void\n} {\n const middleware = (req: MinReq, res: MinRes, next: NextFn): void => {\n const scope = options.scope?.(req)\n const priority = typeof options.priority === 'function'\n ? options.priority(req)\n : options.priority\n\n const ctx: RateLimiterRequestContext = {\n ...(scope !== undefined && { scope }),\n ...(priority !== undefined && { priority }),\n }\n ;(req as Record<string, unknown>)['rateLimiter'] = ctx\n\n if (options.injectHeaders && !res.headersSent) {\n const modelId = typeof options.injectHeaders === 'function'\n ? options.injectHeaders(req)\n : options.injectHeaders\n const status = limiter.getStatus()\n const modelStat = status.models.find(m => m.modelId === modelId)\n\n if (modelStat) {\n res.setHeader('X-RateLimit-Model', modelId)\n res.setHeader('X-RateLimit-Queue-Depth', modelStat.queueDepth)\n res.setHeader('X-RateLimit-Requests-Window', modelStat.requestsInWindow)\n if (modelStat.estimatedWaitMs > 0) {\n res.setHeader('X-RateLimit-Estimated-Wait-Ms', modelStat.estimatedWaitMs)\n }\n }\n }\n\n next()\n }\n\n const errorHandler = (err: unknown, _req: MinReq, res: MinRes, next: NextFn): void => {\n if (!(err instanceof RateLimiterError)) { next(err); return }\n if (res.headersSent) { next(err); return }\n const { status, body } = mapErrorToResponse(err)\n res.status(status).json(body)\n }\n\n return { middleware, errorHandler }\n}\n\n/**\n * Standalone Express 4-argument error handler.\n * Use this when you only need error handling and not scope injection.\n *\n * @example\n * ```typescript\n * app.use(createRateLimiterErrorHandler({ includeDetails: false }))\n * ```\n */\nexport function createRateLimiterErrorHandler(\n options: ErrorHandlerOptions = {},\n): (err: unknown, req: MinReq, res: MinRes, next: NextFn) => void {\n return (err, _req, res, next) => {\n if (!(err instanceof RateLimiterError)) { next(err); return }\n if (res.headersSent) { next(err); return }\n\n if (options.format) {\n const custom = options.format(err)\n if (custom == null) { next(err); return }\n res.status(custom.status).json(custom.body)\n return\n }\n\n const { status, body } = mapErrorToResponse(err, options.includeDetails)\n res.status(status).json(body)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hono middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal Hono Context interface — structural typing, no hard `hono` dep.\n */\nexport interface HonoContext {\n req: {\n raw: Request\n header(name: string): string | undefined\n }\n set(key: string, value: unknown): void\n json(body: unknown, status?: number): Response\n header(name: string, value: string): void\n var: Record<string, unknown>\n}\n\ntype HonoNext = () => Promise<Response | void>\n\n/** Hono middleware handler signature */\nexport type HonoMiddlewareHandler = (c: HonoContext, next: HonoNext) => Promise<Response | void>\n\nexport interface HonoMiddlewareOptions {\n /**\n * Extract scope from the Hono context. Stored in c.var.rateLimiter.scope.\n *\n * @example (c) => c.req.header('x-user-id')\n * @example (c) => c.var.user?.id ? `user:${c.var.user.id}` : undefined\n */\n scope?: (c: HonoContext) => string | undefined\n\n /** Default queue priority, or derive it per-request. */\n priority?: Priority | ((c: HonoContext) => Priority)\n\n /** Inject X-RateLimit-* headers. Pass model ID or function. */\n injectHeaders?: string | ((c: HonoContext) => string)\n}\n\n/**\n * Hono middleware that attaches rateLimiter context and catches RateLimiterErrors.\n *\n * Access the context in route handlers via `c.var.rateLimiter`.\n */\nexport function createHonoMiddleware(\n limiter: RateLimiter,\n options: HonoMiddlewareOptions = {},\n): HonoMiddlewareHandler {\n return async (c, next) => {\n const scope = options.scope?.(c)\n const priority = typeof options.priority === 'function'\n ? options.priority(c)\n : options.priority\n\n const ctx: RateLimiterRequestContext = {\n ...(scope !== undefined && { scope }),\n ...(priority !== undefined && { priority }),\n }\n c.set('rateLimiter', ctx)\n\n if (options.injectHeaders) {\n const modelId = typeof options.injectHeaders === 'function'\n ? options.injectHeaders(c)\n : options.injectHeaders\n const status = limiter.getStatus()\n const modelStat = status.models.find(m => m.modelId === modelId)\n\n if (modelStat) {\n c.header('X-RateLimit-Model', modelId)\n c.header('X-RateLimit-Queue-Depth', String(modelStat.queueDepth))\n c.header('X-RateLimit-Requests-Window', String(modelStat.requestsInWindow))\n if (modelStat.estimatedWaitMs > 0) {\n c.header('X-RateLimit-Estimated-Wait-Ms', String(modelStat.estimatedWaitMs))\n }\n }\n }\n\n try {\n await next()\n } catch (err) {\n if (err instanceof RateLimiterError) {\n const { status, body } = mapErrorToResponse(err)\n return c.json(body, status as Parameters<typeof c.json>[1])\n }\n throw err\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared: error → HTTP response\n// ---------------------------------------------------------------------------\n\n/**\n * Map any RateLimiterError to an HTTP status code + JSON body.\n *\n * Exported so you can use it in custom error handlers, non-Express frameworks,\n * or API gateway integrations.\n *\n * @example\n * ```typescript\n * import { mapErrorToResponse } from 'ai-sdk-rate-limiter/middleware'\n *\n * // Fastify onError hook\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof RateLimiterError) {\n * const { status, body } = mapErrorToResponse(err)\n * return reply.status(status).send(body)\n * }\n * reply.send(err)\n * })\n * ```\n */\nexport function mapErrorToResponse(\n err: RateLimiterError,\n includeDetails = true,\n): { status: number; body: Record<string, unknown> } {\n if (err instanceof QueueTimeoutError) {\n return {\n status: 503,\n body: {\n error: 'Request queued too long. Try again shortly.',\n code: 'QUEUE_TIMEOUT',\n ...(includeDetails && {\n retryAfterMs: 5_000,\n queueDepth: err.queueDepth,\n }),\n },\n }\n }\n\n if (err instanceof QueueFullError) {\n return {\n status: 503,\n body: {\n error: 'Server is busy. Try again in a moment.',\n code: 'QUEUE_FULL',\n },\n }\n }\n\n if (err instanceof BudgetExceededError) {\n return {\n status: 402,\n body: {\n error: 'AI usage budget exceeded.',\n code: 'BUDGET_EXCEEDED',\n ...(includeDetails && {\n period: err.period,\n limitUsd: err.limitUsd,\n currentCostUsd: err.currentCostUsd,\n }),\n },\n }\n }\n\n if (err instanceof CircuitOpenError) {\n return {\n status: 503,\n body: {\n error: 'AI provider temporarily unavailable.',\n code: 'CIRCUIT_OPEN',\n ...(includeDetails && {\n retryAfter: Math.max(0, Math.ceil((err.openUntilMs - Date.now()) / 1000)),\n }),\n },\n }\n }\n\n if (err instanceof ShutdownError) {\n return {\n status: 503,\n body: {\n error: 'Service is shutting down.',\n code: 'SHUTDOWN',\n },\n }\n }\n\n return {\n status: 429,\n body: {\n error: 'Rate limit exceeded.',\n code: 'RATE_LIMITED',\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-sdk-rate-limiter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Smart rate limiting, queuing, and cost tracking middleware for AI SDK calls. Works across providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -66,6 +66,16 @@
|
|
|
66
66
|
"types": "./dist/statsd.d.cts",
|
|
67
67
|
"default": "./dist/statsd.cjs"
|
|
68
68
|
}
|
|
69
|
+
},
|
|
70
|
+
"./middleware": {
|
|
71
|
+
"import": {
|
|
72
|
+
"types": "./dist/middleware.d.ts",
|
|
73
|
+
"default": "./dist/middleware.js"
|
|
74
|
+
},
|
|
75
|
+
"require": {
|
|
76
|
+
"types": "./dist/middleware.d.cts",
|
|
77
|
+
"default": "./dist/middleware.cjs"
|
|
78
|
+
}
|
|
69
79
|
}
|
|
70
80
|
},
|
|
71
81
|
"bin": {
|
|
@@ -97,6 +107,7 @@
|
|
|
97
107
|
"license": "MIT",
|
|
98
108
|
"devDependencies": {
|
|
99
109
|
"@ai-sdk/provider": "^3.0.8",
|
|
110
|
+
"@types/node": "^22.19.15",
|
|
100
111
|
"ai": "^6.0.0",
|
|
101
112
|
"tsup": "^8.5.0",
|
|
102
113
|
"typescript": "^5.8.0",
|
|
@@ -104,8 +115,8 @@
|
|
|
104
115
|
},
|
|
105
116
|
"peerDependencies": {
|
|
106
117
|
"@ai-sdk/provider": ">=1.0.0",
|
|
107
|
-
"
|
|
108
|
-
"
|
|
118
|
+
"@opentelemetry/api": ">=1.0.0",
|
|
119
|
+
"ioredis": ">=4.0.0"
|
|
109
120
|
},
|
|
110
121
|
"peerDependenciesMeta": {
|
|
111
122
|
"@ai-sdk/provider": {
|