@relayplane/proxy 1.8.4 → 1.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/rate-limiter.d.ts +87 -9
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +262 -28
- package/dist/rate-limiter.js.map +1 -1
- package/dist/standalone-proxy.d.ts.map +1 -1
- package/dist/standalone-proxy.js +26 -16
- package/dist/standalone-proxy.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,30 @@ export interface MeshConfigSection {
|
|
|
14
14
|
sync_interval_ms: number;
|
|
15
15
|
contribute: boolean;
|
|
16
16
|
}
|
|
17
|
+
export interface RateLimitModelConfig {
|
|
18
|
+
/** Requests per minute for this model */
|
|
19
|
+
rpm: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderRateLimitConfig {
|
|
22
|
+
/**
|
|
23
|
+
* Requests per minute for ALL models from this provider.
|
|
24
|
+
* Applies when no model-specific override exists.
|
|
25
|
+
* Example: providers.anthropic.rateLimit.rpm = 100
|
|
26
|
+
*/
|
|
27
|
+
rpm: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ProviderConfig {
|
|
30
|
+
/** Provider-level rate limit. Applies to all models for this provider unless overridden per-model. */
|
|
31
|
+
rateLimit?: ProviderRateLimitConfig;
|
|
32
|
+
}
|
|
33
|
+
export interface RateLimitConfigSection {
|
|
34
|
+
/** Per-model RPM overrides. Keys are model names (e.g. "claude-sonnet-4-6"). */
|
|
35
|
+
models?: Record<string, RateLimitModelConfig>;
|
|
36
|
+
/** Max requests to queue when limit is hit (default: 50) */
|
|
37
|
+
maxQueueDepth?: number;
|
|
38
|
+
/** Max ms a queued request waits before getting a 429 (default: 30000) */
|
|
39
|
+
queueTimeoutMs?: number;
|
|
40
|
+
}
|
|
17
41
|
export interface ProxyConfig {
|
|
18
42
|
/** Anonymous device ID (generated on first run) */
|
|
19
43
|
device_id: string;
|
|
@@ -31,6 +55,23 @@ export interface ProxyConfig {
|
|
|
31
55
|
updated_at: string;
|
|
32
56
|
/** Mesh (Osmosis) learning layer config */
|
|
33
57
|
mesh?: MeshConfigSection;
|
|
58
|
+
/** Rate limiter configuration */
|
|
59
|
+
rateLimit?: RateLimitConfigSection;
|
|
60
|
+
/**
|
|
61
|
+
* Per-provider configuration.
|
|
62
|
+
* Supported providers: anthropic, openai, google, xai, groq, perplexity.
|
|
63
|
+
*
|
|
64
|
+
* Example ~/.relayplane/config.json:
|
|
65
|
+
* ```json
|
|
66
|
+
* {
|
|
67
|
+
* "providers": {
|
|
68
|
+
* "anthropic": { "rateLimit": { "rpm": 100 } },
|
|
69
|
+
* "openai": { "rateLimit": { "rpm": 60 } }
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
providers?: Record<string, ProviderConfig>;
|
|
34
75
|
}
|
|
35
76
|
/**
|
|
36
77
|
* Check if credentials.json exists and has a valid apiKey
|
|
@@ -103,4 +144,13 @@ export declare function getMeshConfig(): MeshConfigSection;
|
|
|
103
144
|
* Update mesh config section
|
|
104
145
|
*/
|
|
105
146
|
export declare function updateMeshConfig(updates: Partial<MeshConfigSection>): void;
|
|
147
|
+
/**
|
|
148
|
+
* Get rate limit config section (with defaults)
|
|
149
|
+
*/
|
|
150
|
+
export declare function getRateLimitConfig(): RateLimitConfigSection;
|
|
151
|
+
/**
|
|
152
|
+
* Get per-provider configurations (with defaults).
|
|
153
|
+
* Returns empty object if no providers section exists in config.
|
|
154
|
+
*/
|
|
155
|
+
export declare function getProviderConfigs(): Record<string, ProviderConfig>;
|
|
106
156
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAElB,8BAA8B;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAE3B,kDAAkD;IAClD,kBAAkB,EAAE,OAAO,CAAC;IAE5B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAC;IAEvB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;IAEnB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -57,6 +57,8 @@ exports.getConfigPath = getConfigPath;
|
|
|
57
57
|
exports.getCredentialsPath = getCredentialsPath;
|
|
58
58
|
exports.getMeshConfig = getMeshConfig;
|
|
59
59
|
exports.updateMeshConfig = updateMeshConfig;
|
|
60
|
+
exports.getRateLimitConfig = getRateLimitConfig;
|
|
61
|
+
exports.getProviderConfigs = getProviderConfigs;
|
|
60
62
|
const fs = __importStar(require("fs"));
|
|
61
63
|
const path = __importStar(require("path"));
|
|
62
64
|
const os = __importStar(require("os"));
|
|
@@ -314,4 +316,19 @@ function updateMeshConfig(updates) {
|
|
|
314
316
|
config.mesh = { ...getMeshConfig(), ...updates };
|
|
315
317
|
saveConfig(config);
|
|
316
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Get rate limit config section (with defaults)
|
|
321
|
+
*/
|
|
322
|
+
function getRateLimitConfig() {
|
|
323
|
+
const config = loadConfig();
|
|
324
|
+
return config.rateLimit ?? {};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get per-provider configurations (with defaults).
|
|
328
|
+
* Returns empty object if no providers section exists in config.
|
|
329
|
+
*/
|
|
330
|
+
function getProviderConfigs() {
|
|
331
|
+
const config = loadConfig();
|
|
332
|
+
return config.providers ?? {};
|
|
333
|
+
}
|
|
317
334
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6IH,kDAQC;AAOD,gCAmEC;AAMD,gCAiBC;AAKD,oCAKC;AAKD,gCAGC;AAKD,oDAEC;AAKD,gDAGC;AAKD,0CAEC;AAKD,4CAEC;AAKD,kCAGC;AAKD,8BAaC;AAKD,8BAGC;AAKD,oCAEC;AAKD,sCAEC;AAKD,gDAEC;AAKD,sCAQC;AAKD,4CAIC;AAKD,gDAGC;AAMD,gDAGC;AAjYD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AACzB,+CAAiC;AAqFjC,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;AAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;AAC5D,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAEnE;;;GAGG;AACH,SAAS,gBAAgB;IACvB,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3E,OAAO,QAAQ,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,SAAS,eAAe;IACtB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB;IAC1B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO;QACL,SAAS,EAAE,gBAAgB,EAAE;QAC7B,iBAAiB,EAAE,KAAK,EAAE,wDAAwD;QAClF,kBAAkB,EAAE,KAAK;QACzB,cAAc,EAAE,cAAc;QAC9B,UAAU,EAAE,GAAG;QACf,UAAU,EAAE,GAAG;QACf,IAAI,EAAE;YACJ,OAAO,EAAE,KAAK,EAAE,8DAA8D;YAC9E,QAAQ,EAAE,kCAAkC;YAC5C,gBAAgB,EAAE,KAAK;YACvB,UAAU,EAAE,IAAI;SACjB;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,mBAAmB;IACjC,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;YACrE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU;IACxB,eAAe,EAAE,CAAC;IAElB,qBAAqB;IACrB,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAC;YAE/C,gDAAgD;YAChD,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBACtB,MAAM,CAAC,SAAS,GAAG,gBAAgB,EAAE,CAAC;YACxC,CAAC;YACD,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;gBAC3C,MAAM,CAAC,iBAAiB,GAAG,KAAK,CAAC;YACnC,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAC3B,MAAM,CAAC,cAAc,GAAG,cAAc,CAAC;YACzC,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,kCAAkC;YAClC,OAAO,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IAED,oBAAoB;IACpB,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,sFAAsF,CAAC,CAAC;YAErG,4EAA4E;YAC5E,IAAI,mBAAmB,EAAE,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;YAC7E,CAAC;YAED,gCAAgC;YAChC,UAAU,CAAC,MAAM,CAAC,CAAC;YACnB,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;QAChG,CAAC;IACH,CAAC;IAED,6DAA6D;IAC7D,IAAI,mBAAmB,EAAE,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;IAC7E,CAAC;IAED,qCAAqC;IACrC,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;IAErC,wDAAwD;IACxD,IAAI,mBAAmB,EAAE,EAAE,CAAC;QAC1B,MAAM,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;QACnE,2CAA2C;QAC3C,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,UAAU,CAAC,MAAM,CAAC,CAAC;IACnB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAgB,UAAU,CAAC,MAAmB;IAC5C,eAAe,EAAE,CAAC;IAClB,MAAM,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE7C,2CAA2C;IAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7C,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACnC,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,SAAgB,YAAY,CAAC,OAA6B;IACxD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,CAAC,MAAM,CAAC,CAAC;IACnB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,UAAU;IACxB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,SAAgB,oBAAoB;IAClC,YAAY,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB;IAChC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,iBAAiB,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe;IAC7B,YAAY,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB;IAC9B,YAAY,CAAC,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;GAEG;AACH,SAAgB,WAAW;IACzB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,SAAS,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,SAAgB,SAAS,CAAC,GAAW;IACnC,YAAY,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAE/B,yEAAyE;IACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IAC3D,IAAI,CAAC;QACH,IAAI,KAAK,GAAwB,EAAE,CAAC;QACpC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC;QACnB,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;AACZ,CAAC;AAED;;GAEG;AACH,SAAgB,SAAS;IACvB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,OAAO,CAAC;AACxB,CAAC;AAED;;GAEG;AACH,SAAgB,YAAY;IAC1B,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,SAAgB,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB;IAChC,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,SAAgB,aAAa;IAC3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,IAAI,IAAI;QACpB,OAAO,EAAE,KAAK;QACd,QAAQ,EAAE,kCAAkC;QAC5C,gBAAgB,EAAE,KAAK;QACvB,UAAU,EAAE,IAAI;KACjB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB,CAAC,OAAmC;IAClE,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,aAAa,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IACjD,UAAU,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB;IAChC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB;IAChC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;AAChC,CAAC"}
|
package/dist/rate-limiter.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rate Limiter - In-memory rate limiting for RelayPlane Proxy
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Limits are configurable via ~/.relayplane/config.json under `rateLimit.models`.
|
|
5
|
+
* When a limit is hit, requests are queued (up to maxQueueDepth) instead of
|
|
6
|
+
* immediately returning 429. Queue overflow or timeout results in a 429.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Defaults:
|
|
9
|
+
* - Sonnet models: 60 RPM
|
|
10
|
+
* - Opus models: 30 RPM
|
|
11
|
+
* - Haiku models: 60 RPM
|
|
12
|
+
* - Other models: 60 RPM
|
|
13
|
+
*
|
|
14
|
+
* Auto-expires old entries every 5 minutes.
|
|
9
15
|
*/
|
|
16
|
+
import type { RateLimitConfigSection, ProviderConfig } from './config.js';
|
|
10
17
|
export interface RateLimitConfig {
|
|
11
18
|
rpm: number;
|
|
12
19
|
maxTokens?: number;
|
|
@@ -18,15 +25,77 @@ export interface RateLimitCheck {
|
|
|
18
25
|
limit: number;
|
|
19
26
|
retryAfter?: number;
|
|
20
27
|
}
|
|
28
|
+
export declare class RateLimitError extends Error {
|
|
29
|
+
readonly code: 'QUEUE_FULL' | 'QUEUE_TIMEOUT';
|
|
30
|
+
readonly retryAfter: number;
|
|
31
|
+
readonly limit: number;
|
|
32
|
+
readonly resetAt: number;
|
|
33
|
+
constructor(message: string, opts: {
|
|
34
|
+
code: 'QUEUE_FULL' | 'QUEUE_TIMEOUT';
|
|
35
|
+
retryAfter: number;
|
|
36
|
+
limit: number;
|
|
37
|
+
resetAt: number;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
21
40
|
export declare const DEFAULT_LIMITS: Record<string, RateLimitConfig>;
|
|
22
|
-
declare class RateLimiter {
|
|
41
|
+
export declare class RateLimiter {
|
|
23
42
|
private buckets;
|
|
43
|
+
private queues;
|
|
44
|
+
private drainTimers;
|
|
24
45
|
private lastCleanup;
|
|
25
46
|
private readonly CLEANUP_INTERVAL;
|
|
47
|
+
private modelOverrides;
|
|
48
|
+
/**
|
|
49
|
+
* Provider-level overrides. Key is provider name (e.g. "anthropic").
|
|
50
|
+
* Applied when no model-specific override exists.
|
|
51
|
+
* Set via config.json `providers.{name}.rateLimit.rpm`.
|
|
52
|
+
*/
|
|
53
|
+
private providerOverrides;
|
|
54
|
+
private maxQueueDepth;
|
|
55
|
+
private queueTimeoutMs;
|
|
56
|
+
constructor(opts?: {
|
|
57
|
+
maxQueueDepth?: number;
|
|
58
|
+
queueTimeoutMs?: number;
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Apply configuration from ~/.relayplane/config.json rateLimit section.
|
|
62
|
+
* Call once at proxy startup.
|
|
63
|
+
*/
|
|
64
|
+
configure(cfg: RateLimitConfigSection): void;
|
|
26
65
|
/**
|
|
27
|
-
*
|
|
66
|
+
* Apply per-provider configuration from ~/.relayplane/config.json `providers` section.
|
|
67
|
+
* Call once at proxy startup, after configure().
|
|
68
|
+
*
|
|
69
|
+
* Each provider's rateLimit.rpm becomes the fallback for ALL models from that provider
|
|
70
|
+
* when no model-specific override exists.
|
|
71
|
+
*
|
|
72
|
+
* Example config.json:
|
|
73
|
+
* ```json
|
|
74
|
+
* { "providers": { "anthropic": { "rateLimit": { "rpm": 100 } } } }
|
|
75
|
+
* ```
|
|
28
76
|
*/
|
|
29
|
-
|
|
77
|
+
configureProviders(providers: Record<string, ProviderConfig>): void;
|
|
78
|
+
/**
|
|
79
|
+
* Synchronous check — returns immediately without queuing.
|
|
80
|
+
* Increments counter if allowed.
|
|
81
|
+
*
|
|
82
|
+
* @param provider Optional provider name (e.g. "anthropic"). Used to look up
|
|
83
|
+
* provider-level RPM limits when no model-specific override exists.
|
|
84
|
+
* Limits are isolated per-provider — one provider hitting its cap
|
|
85
|
+
* does NOT affect other providers.
|
|
86
|
+
*/
|
|
87
|
+
checkLimit(workspaceId: string, model: string, provider?: string): RateLimitCheck;
|
|
88
|
+
/**
|
|
89
|
+
* Async slot acquisition with queuing.
|
|
90
|
+
*
|
|
91
|
+
* - Resolves immediately if a slot is available.
|
|
92
|
+
* - Queues the request if the limit is hit (up to maxQueueDepth).
|
|
93
|
+
* - Throws RateLimitError with code QUEUE_FULL if the queue is full.
|
|
94
|
+
* - Throws RateLimitError with code QUEUE_TIMEOUT if the request waits too long.
|
|
95
|
+
*/
|
|
96
|
+
acquireSlot(workspaceId: string, model: string, provider?: string): Promise<void>;
|
|
97
|
+
private scheduleDrain;
|
|
98
|
+
private drainQueue;
|
|
30
99
|
/**
|
|
31
100
|
* Get current usage for a workspace/model
|
|
32
101
|
*/
|
|
@@ -39,6 +108,10 @@ declare class RateLimiter {
|
|
|
39
108
|
* Reset limit for a specific workspace/model (emergency use)
|
|
40
109
|
*/
|
|
41
110
|
resetLimit(workspaceId: string, model?: string): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get current queue depth for a workspace/model
|
|
113
|
+
*/
|
|
114
|
+
getQueueDepth(workspaceId: string, model: string): number;
|
|
42
115
|
/**
|
|
43
116
|
* Get all active limits (for debugging)
|
|
44
117
|
*/
|
|
@@ -53,12 +126,17 @@ declare class RateLimiter {
|
|
|
53
126
|
private maybeCleanup;
|
|
54
127
|
}
|
|
55
128
|
export declare const rateLimiter: RateLimiter;
|
|
56
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Load rateLimit + providers config from ~/.relayplane/config.json and apply to the singleton.
|
|
131
|
+
* Call once at proxy startup.
|
|
132
|
+
*/
|
|
133
|
+
export declare function configureRateLimiter(): void;
|
|
134
|
+
export declare const checkLimit: (workspaceId: string, model: string, provider?: string) => RateLimitCheck;
|
|
135
|
+
export declare const acquireSlot: (workspaceId: string, model: string, provider?: string) => Promise<void>;
|
|
57
136
|
export declare const getUsage: (workspaceId: string, model: string) => {
|
|
58
137
|
used: number;
|
|
59
138
|
limit: number;
|
|
60
139
|
resetAt: number;
|
|
61
140
|
};
|
|
62
141
|
export declare const resetLimit: (workspaceId: string, model?: string) => void;
|
|
63
|
-
export {};
|
|
64
142
|
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAuC1E,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,IAAI,EAAE,YAAY,GAAG,eAAe,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CASrG;AAGD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAe1D,CAAC;AAcF,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAoD;IACvE,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiB;IAElD,OAAO,CAAC,cAAc,CAAuC;IAC7D;;;;OAIG;IACH,OAAO,CAAC,iBAAiB,CAAuC;IAChE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAAS;gBAEnB,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE;IAKtE;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,sBAAsB,GAAG,IAAI;IAc5C;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI;IAQnE;;;;;;;;OAQG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc;IAsCjF;;;;;;;OAOG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6DvF,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,UAAU;IAgBlB;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAY9F;;OAEG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAYrD;;OAEG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAKzD;;OAEG;IACH,eAAe,IAAI,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAQzE,OAAO,CAAC,SAAS;IAkCjB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,YAAY;CAYrB;AAGD,eAAO,MAAM,WAAW,aAAoB,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAa3C;AAGD,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,EAAE,WAAW,MAAM,KAAG,cAC7B,CAAC;AAEvD,eAAO,MAAM,WAAW,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,IAAI,CACzC,CAAC;AAExD,eAAO,MAAM,QAAQ,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM;UA3IH,MAAM;WAAS,MAAM;aAAW,MAAM;CA4IpD,CAAC;AAE3C,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,EAAE,QAAQ,MAAM,KAAG,IACrB,CAAC"}
|
package/dist/rate-limiter.js
CHANGED
|
@@ -2,20 +2,77 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Rate Limiter - In-memory rate limiting for RelayPlane Proxy
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Limits are configurable via ~/.relayplane/config.json under `rateLimit.models`.
|
|
6
|
+
* When a limit is hit, requests are queued (up to maxQueueDepth) instead of
|
|
7
|
+
* immediately returning 429. Queue overflow or timeout results in a 429.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Defaults:
|
|
10
|
+
* - Sonnet models: 60 RPM
|
|
11
|
+
* - Opus models: 30 RPM
|
|
12
|
+
* - Haiku models: 60 RPM
|
|
13
|
+
* - Other models: 60 RPM
|
|
14
|
+
*
|
|
15
|
+
* Auto-expires old entries every 5 minutes.
|
|
10
16
|
*/
|
|
11
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.resetLimit = exports.getUsage = exports.checkLimit = exports.rateLimiter = exports.DEFAULT_LIMITS = void 0;
|
|
13
|
-
|
|
18
|
+
exports.resetLimit = exports.getUsage = exports.acquireSlot = exports.checkLimit = exports.rateLimiter = exports.RateLimiter = exports.DEFAULT_LIMITS = exports.RateLimitError = void 0;
|
|
19
|
+
exports.configureRateLimiter = configureRateLimiter;
|
|
20
|
+
// ── Sanitizers (defence against Infinity / NaN / negative values) ────────────
|
|
21
|
+
/** Maximum configurable RPM — prevents Infinity from bypassing the limiter. */
|
|
22
|
+
const MAX_RPM = 10_000;
|
|
23
|
+
/** Maximum configurable queue depth. */
|
|
24
|
+
const MAX_QUEUE_DEPTH = 10_000;
|
|
25
|
+
/** Maximum configurable queue timeout (10 minutes). */
|
|
26
|
+
const MAX_QUEUE_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
27
|
+
/**
|
|
28
|
+
* Clamp an RPM value to a safe finite integer in [1, MAX_RPM].
|
|
29
|
+
* Returns the safe default (60) when the input is 0, negative, NaN, or Infinity.
|
|
30
|
+
*/
|
|
31
|
+
function sanitizeRpm(value, safeDefault = 60) {
|
|
32
|
+
const num = Number(value);
|
|
33
|
+
if (!Number.isFinite(num) || num < 1)
|
|
34
|
+
return safeDefault;
|
|
35
|
+
return Math.min(MAX_RPM, Math.floor(num));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a maxQueueDepth value to a safe finite integer in [0, MAX_QUEUE_DEPTH].
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeQueueDepth(value, safeDefault = 50) {
|
|
41
|
+
const num = Number(value);
|
|
42
|
+
if (!Number.isFinite(num) || num < 0)
|
|
43
|
+
return safeDefault;
|
|
44
|
+
return Math.min(MAX_QUEUE_DEPTH, Math.floor(num));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Clamp a queueTimeoutMs value to a safe finite integer in [100, MAX_QUEUE_TIMEOUT_MS].
|
|
48
|
+
*/
|
|
49
|
+
function sanitizeTimeoutMs(value, safeDefault = 30_000) {
|
|
50
|
+
const num = Number(value);
|
|
51
|
+
if (!Number.isFinite(num) || num < 100)
|
|
52
|
+
return safeDefault;
|
|
53
|
+
return Math.min(MAX_QUEUE_TIMEOUT_MS, Math.floor(num));
|
|
54
|
+
}
|
|
55
|
+
class RateLimitError extends Error {
|
|
56
|
+
code;
|
|
57
|
+
retryAfter;
|
|
58
|
+
limit;
|
|
59
|
+
resetAt;
|
|
60
|
+
constructor(message, opts) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = 'RateLimitError';
|
|
63
|
+
this.code = opts.code;
|
|
64
|
+
this.retryAfter = opts.retryAfter;
|
|
65
|
+
this.limit = opts.limit;
|
|
66
|
+
this.resetAt = opts.resetAt;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.RateLimitError = RateLimitError;
|
|
70
|
+
// Default limits. Sonnet bumped to 60 RPM, Opus bumped to 30 RPM (GH #39).
|
|
14
71
|
exports.DEFAULT_LIMITS = {
|
|
15
72
|
// Anthropic models
|
|
16
|
-
'claude-opus-4-6': { rpm:
|
|
17
|
-
'claude-opus': { rpm:
|
|
18
|
-
'claude-sonnet-4-6': { rpm:
|
|
73
|
+
'claude-opus-4-6': { rpm: 30, maxTokens: 4096 },
|
|
74
|
+
'claude-opus': { rpm: 30, maxTokens: 4096 },
|
|
75
|
+
'claude-sonnet-4-6': { rpm: 60 },
|
|
19
76
|
'claude-haiku-4-5': { rpm: 60 },
|
|
20
77
|
// OpenAI models
|
|
21
78
|
'gpt-4o': { rpm: 30 },
|
|
@@ -23,28 +80,85 @@ exports.DEFAULT_LIMITS = {
|
|
|
23
80
|
'o1': { rpm: 10, maxTokens: 4096 },
|
|
24
81
|
'o3-mini': { rpm: 30 },
|
|
25
82
|
// Default for unknown models
|
|
26
|
-
'default': { rpm: 60 }
|
|
83
|
+
'default': { rpm: 60 },
|
|
27
84
|
};
|
|
28
85
|
class RateLimiter {
|
|
29
86
|
buckets = new Map();
|
|
87
|
+
queues = new Map();
|
|
88
|
+
drainTimers = new Map();
|
|
30
89
|
lastCleanup = Date.now();
|
|
31
90
|
CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
|
91
|
+
modelOverrides = {};
|
|
92
|
+
/**
|
|
93
|
+
* Provider-level overrides. Key is provider name (e.g. "anthropic").
|
|
94
|
+
* Applied when no model-specific override exists.
|
|
95
|
+
* Set via config.json `providers.{name}.rateLimit.rpm`.
|
|
96
|
+
*/
|
|
97
|
+
providerOverrides = {};
|
|
98
|
+
maxQueueDepth;
|
|
99
|
+
queueTimeoutMs;
|
|
100
|
+
constructor(opts) {
|
|
101
|
+
this.maxQueueDepth = opts?.maxQueueDepth ?? 50;
|
|
102
|
+
this.queueTimeoutMs = opts?.queueTimeoutMs ?? 30_000;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Apply configuration from ~/.relayplane/config.json rateLimit section.
|
|
106
|
+
* Call once at proxy startup.
|
|
107
|
+
*/
|
|
108
|
+
configure(cfg) {
|
|
109
|
+
if (cfg.models) {
|
|
110
|
+
for (const [model, modelCfg] of Object.entries(cfg.models)) {
|
|
111
|
+
this.modelOverrides[model.toLowerCase()] = { rpm: sanitizeRpm(modelCfg.rpm) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (cfg.maxQueueDepth !== undefined) {
|
|
115
|
+
this.maxQueueDepth = sanitizeQueueDepth(cfg.maxQueueDepth);
|
|
116
|
+
}
|
|
117
|
+
if (cfg.queueTimeoutMs !== undefined) {
|
|
118
|
+
this.queueTimeoutMs = sanitizeTimeoutMs(cfg.queueTimeoutMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Apply per-provider configuration from ~/.relayplane/config.json `providers` section.
|
|
123
|
+
* Call once at proxy startup, after configure().
|
|
124
|
+
*
|
|
125
|
+
* Each provider's rateLimit.rpm becomes the fallback for ALL models from that provider
|
|
126
|
+
* when no model-specific override exists.
|
|
127
|
+
*
|
|
128
|
+
* Example config.json:
|
|
129
|
+
* ```json
|
|
130
|
+
* { "providers": { "anthropic": { "rateLimit": { "rpm": 100 } } } }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
configureProviders(providers) {
|
|
134
|
+
for (const [provider, cfg] of Object.entries(providers)) {
|
|
135
|
+
if (cfg.rateLimit?.rpm !== undefined) {
|
|
136
|
+
this.providerOverrides[provider.toLowerCase()] = { rpm: sanitizeRpm(cfg.rateLimit.rpm) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
32
140
|
/**
|
|
33
|
-
*
|
|
141
|
+
* Synchronous check — returns immediately without queuing.
|
|
142
|
+
* Increments counter if allowed.
|
|
143
|
+
*
|
|
144
|
+
* @param provider Optional provider name (e.g. "anthropic"). Used to look up
|
|
145
|
+
* provider-level RPM limits when no model-specific override exists.
|
|
146
|
+
* Limits are isolated per-provider — one provider hitting its cap
|
|
147
|
+
* does NOT affect other providers.
|
|
34
148
|
*/
|
|
35
|
-
checkLimit(workspaceId, model) {
|
|
149
|
+
checkLimit(workspaceId, model, provider) {
|
|
36
150
|
this.maybeCleanup();
|
|
37
|
-
const config = this.getConfig(model);
|
|
38
|
-
const
|
|
151
|
+
const config = this.getConfig(model, provider);
|
|
152
|
+
const providerSeg = provider ? `:${provider.toLowerCase()}` : '';
|
|
153
|
+
const key = `${workspaceId}${providerSeg}:${this.getModelKey(model)}:${this.getCurrentMinute()}`;
|
|
39
154
|
const now = Date.now();
|
|
40
|
-
const windowMs = 60 * 1000;
|
|
155
|
+
const windowMs = 60 * 1000;
|
|
41
156
|
const resetAt = this.getCurrentMinute() + windowMs;
|
|
42
157
|
let entry = this.buckets.get(key);
|
|
43
158
|
if (!entry) {
|
|
44
159
|
entry = { count: 0, resetAt };
|
|
45
160
|
this.buckets.set(key, entry);
|
|
46
161
|
}
|
|
47
|
-
// Check if window expired and reset
|
|
48
162
|
if (now > entry.resetAt) {
|
|
49
163
|
entry.count = 0;
|
|
50
164
|
entry.resetAt = resetAt;
|
|
@@ -59,9 +173,91 @@ class RateLimiter {
|
|
|
59
173
|
remaining,
|
|
60
174
|
resetAt,
|
|
61
175
|
limit: config.rpm,
|
|
62
|
-
retryAfter: allowed ? undefined : Math.ceil((resetAt - now) / 1000)
|
|
176
|
+
retryAfter: allowed ? undefined : Math.ceil((resetAt - now) / 1000),
|
|
63
177
|
};
|
|
64
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Async slot acquisition with queuing.
|
|
181
|
+
*
|
|
182
|
+
* - Resolves immediately if a slot is available.
|
|
183
|
+
* - Queues the request if the limit is hit (up to maxQueueDepth).
|
|
184
|
+
* - Throws RateLimitError with code QUEUE_FULL if the queue is full.
|
|
185
|
+
* - Throws RateLimitError with code QUEUE_TIMEOUT if the request waits too long.
|
|
186
|
+
*/
|
|
187
|
+
async acquireSlot(workspaceId, model, provider) {
|
|
188
|
+
const check = this.checkLimit(workspaceId, model, provider);
|
|
189
|
+
if (check.allowed)
|
|
190
|
+
return;
|
|
191
|
+
const providerSeg = provider ? `:${provider.toLowerCase()}` : '';
|
|
192
|
+
const queueKey = `${workspaceId}${providerSeg}:${this.getModelKey(model)}`;
|
|
193
|
+
if (!this.queues.has(queueKey)) {
|
|
194
|
+
this.queues.set(queueKey, []);
|
|
195
|
+
}
|
|
196
|
+
const queue = this.queues.get(queueKey);
|
|
197
|
+
if (queue.length >= this.maxQueueDepth) {
|
|
198
|
+
throw new RateLimitError(`Rate limit queue full for ${model}. Max queue depth (${this.maxQueueDepth}) reached. ` +
|
|
199
|
+
`Retry after ${check.retryAfter ?? 60}s.`, {
|
|
200
|
+
code: 'QUEUE_FULL',
|
|
201
|
+
retryAfter: check.retryAfter ?? 60,
|
|
202
|
+
limit: check.limit,
|
|
203
|
+
resetAt: check.resetAt,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Schedule drain at window reset (only one timer per queue key)
|
|
207
|
+
this.scheduleDrain(queueKey, workspaceId, model, check.resetAt, provider);
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const timeoutId = setTimeout(() => {
|
|
210
|
+
const idx = queue.indexOf(entry);
|
|
211
|
+
if (idx >= 0)
|
|
212
|
+
queue.splice(idx, 1);
|
|
213
|
+
reject(new RateLimitError(`Rate limit queue timeout for ${model} after ${this.queueTimeoutMs}ms. ` +
|
|
214
|
+
`Retry after ${entry.check.retryAfter ?? 60}s.`, {
|
|
215
|
+
code: 'QUEUE_TIMEOUT',
|
|
216
|
+
retryAfter: entry.check.retryAfter ?? 60,
|
|
217
|
+
limit: entry.check.limit,
|
|
218
|
+
resetAt: entry.check.resetAt,
|
|
219
|
+
}));
|
|
220
|
+
}, this.queueTimeoutMs);
|
|
221
|
+
const entry = {
|
|
222
|
+
resolve: () => {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
resolve();
|
|
225
|
+
},
|
|
226
|
+
reject: (err) => {
|
|
227
|
+
clearTimeout(timeoutId);
|
|
228
|
+
reject(err);
|
|
229
|
+
},
|
|
230
|
+
timeoutId,
|
|
231
|
+
check,
|
|
232
|
+
};
|
|
233
|
+
queue.push(entry);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
scheduleDrain(queueKey, workspaceId, model, resetAt, provider) {
|
|
237
|
+
if (this.drainTimers.has(queueKey))
|
|
238
|
+
return; // already scheduled
|
|
239
|
+
const delay = Math.max(0, resetAt - Date.now());
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
this.drainTimers.delete(queueKey);
|
|
242
|
+
this.drainQueue(queueKey, workspaceId, model, provider);
|
|
243
|
+
}, delay);
|
|
244
|
+
this.drainTimers.set(queueKey, timer);
|
|
245
|
+
}
|
|
246
|
+
drainQueue(queueKey, workspaceId, model, provider) {
|
|
247
|
+
const queue = this.queues.get(queueKey);
|
|
248
|
+
if (!queue || queue.length === 0)
|
|
249
|
+
return;
|
|
250
|
+
while (queue.length > 0) {
|
|
251
|
+
const check = this.checkLimit(workspaceId, model, provider);
|
|
252
|
+
if (!check.allowed) {
|
|
253
|
+
// Window filled up — schedule next drain at next window reset
|
|
254
|
+
this.scheduleDrain(queueKey, workspaceId, model, check.resetAt, provider);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const entry = queue.shift();
|
|
258
|
+
entry.resolve();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
65
261
|
/**
|
|
66
262
|
* Get current usage for a workspace/model
|
|
67
263
|
*/
|
|
@@ -72,7 +268,7 @@ class RateLimiter {
|
|
|
72
268
|
return {
|
|
73
269
|
used: entry?.count || 0,
|
|
74
270
|
limit: config.rpm,
|
|
75
|
-
resetAt: entry?.resetAt || this.getCurrentMinute() + 60 * 1000
|
|
271
|
+
resetAt: entry?.resetAt || this.getCurrentMinute() + 60 * 1000,
|
|
76
272
|
};
|
|
77
273
|
}
|
|
78
274
|
/**
|
|
@@ -88,6 +284,13 @@ class RateLimiter {
|
|
|
88
284
|
}
|
|
89
285
|
}
|
|
90
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Get current queue depth for a workspace/model
|
|
289
|
+
*/
|
|
290
|
+
getQueueDepth(workspaceId, model) {
|
|
291
|
+
const queueKey = `${workspaceId}:${this.getModelKey(model)}`;
|
|
292
|
+
return this.queues.get(queueKey)?.length ?? 0;
|
|
293
|
+
}
|
|
91
294
|
/**
|
|
92
295
|
* Get all active limits (for debugging)
|
|
93
296
|
*/
|
|
@@ -95,18 +298,32 @@ class RateLimiter {
|
|
|
95
298
|
return Array.from(this.buckets.entries()).map(([key, entry]) => ({
|
|
96
299
|
key,
|
|
97
300
|
count: entry.count,
|
|
98
|
-
resetAt: entry.resetAt
|
|
301
|
+
resetAt: entry.resetAt,
|
|
99
302
|
}));
|
|
100
303
|
}
|
|
101
|
-
getConfig(model) {
|
|
102
|
-
// Normalize model name
|
|
304
|
+
getConfig(model, provider) {
|
|
103
305
|
const normalized = model.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
104
|
-
//
|
|
306
|
+
// 1. Model-specific override takes highest priority
|
|
307
|
+
if (this.modelOverrides[normalized]) {
|
|
308
|
+
return this.modelOverrides[normalized];
|
|
309
|
+
}
|
|
310
|
+
// 2. Provider-level override (e.g. providers.anthropic.rateLimit.rpm)
|
|
311
|
+
// Applies to ALL models from this provider when no model-specific override exists.
|
|
312
|
+
// Each provider maintains its own isolated bucket — limits don't cascade across providers.
|
|
313
|
+
if (provider) {
|
|
314
|
+
const providerNorm = provider.toLowerCase();
|
|
315
|
+
if (this.providerOverrides[providerNorm]) {
|
|
316
|
+
return this.providerOverrides[providerNorm];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// 3. Check exact match in built-in defaults
|
|
105
320
|
if (exports.DEFAULT_LIMITS[normalized]) {
|
|
106
321
|
return exports.DEFAULT_LIMITS[normalized];
|
|
107
322
|
}
|
|
108
|
-
//
|
|
323
|
+
// 4. Partial match (e.g. "claude-sonnet" matches "claude-sonnet-4-6")
|
|
109
324
|
for (const [key, config] of Object.entries(exports.DEFAULT_LIMITS)) {
|
|
325
|
+
if (key === 'default')
|
|
326
|
+
continue;
|
|
110
327
|
if (normalized.includes(key) || key.includes(normalized)) {
|
|
111
328
|
return config;
|
|
112
329
|
}
|
|
@@ -135,23 +352,40 @@ class RateLimiter {
|
|
|
135
352
|
}
|
|
136
353
|
maybeCleanup() {
|
|
137
354
|
const now = Date.now();
|
|
138
|
-
if (now - this.lastCleanup < this.CLEANUP_INTERVAL)
|
|
355
|
+
if (now - this.lastCleanup < this.CLEANUP_INTERVAL)
|
|
139
356
|
return;
|
|
140
|
-
}
|
|
141
|
-
// Remove expired entries
|
|
142
357
|
for (const [key, entry] of this.buckets) {
|
|
143
|
-
if (now > entry.resetAt + 60000) {
|
|
358
|
+
if (now > entry.resetAt + 60000) {
|
|
144
359
|
this.buckets.delete(key);
|
|
145
360
|
}
|
|
146
361
|
}
|
|
147
362
|
this.lastCleanup = now;
|
|
148
363
|
}
|
|
149
364
|
}
|
|
365
|
+
exports.RateLimiter = RateLimiter;
|
|
150
366
|
// Singleton instance
|
|
151
367
|
exports.rateLimiter = new RateLimiter();
|
|
368
|
+
/**
|
|
369
|
+
* Load rateLimit + providers config from ~/.relayplane/config.json and apply to the singleton.
|
|
370
|
+
* Call once at proxy startup.
|
|
371
|
+
*/
|
|
372
|
+
function configureRateLimiter() {
|
|
373
|
+
try {
|
|
374
|
+
// Dynamic require to avoid circular deps at module init time
|
|
375
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
376
|
+
const cfg = require('./config.js');
|
|
377
|
+
exports.rateLimiter.configure(cfg.getRateLimitConfig());
|
|
378
|
+
exports.rateLimiter.configureProviders(cfg.getProviderConfigs());
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Ignore — use defaults
|
|
382
|
+
}
|
|
383
|
+
}
|
|
152
384
|
// Convenience exports
|
|
153
|
-
const checkLimit = (workspaceId, model) => exports.rateLimiter.checkLimit(workspaceId, model);
|
|
385
|
+
const checkLimit = (workspaceId, model, provider) => exports.rateLimiter.checkLimit(workspaceId, model, provider);
|
|
154
386
|
exports.checkLimit = checkLimit;
|
|
387
|
+
const acquireSlot = (workspaceId, model, provider) => exports.rateLimiter.acquireSlot(workspaceId, model, provider);
|
|
388
|
+
exports.acquireSlot = acquireSlot;
|
|
155
389
|
const getUsage = (workspaceId, model) => exports.rateLimiter.getUsage(workspaceId, model);
|
|
156
390
|
exports.getUsage = getUsage;
|
|
157
391
|
const resetLimit = (workspaceId, model) => exports.rateLimiter.resetLimit(workspaceId, model);
|