@naced/llm-multi-rate-limiter 0.0.0-stage → 0.1.2
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/CHANGELOG.md +78 -0
- package/LICENSE +22 -0
- package/README.md +112 -2
- package/README.zh.md +115 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +956 -0
- package/lib/config-resolve.js +259 -0
- package/lib/index.d.ts +91 -0
- package/lib/index.js +552 -0
- package/lib/status-rpc.js +285 -0
- package/lib/strategies/sliding-window.js +174 -0
- package/lib/strategies/token-bucket.js +196 -0
- package/lib/types/config.js +126 -0
- package/lib/types/index.js +1 -0
- package/package.json +81 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter configuration schema.
|
|
3
|
+
*
|
|
4
|
+
* The plugin's Config is the profile entry's own schema: Cordis validates the
|
|
5
|
+
* entry's `config` against it, so the host reads values from the references
|
|
6
|
+
* this schema produces rather than from a settings namespace.
|
|
7
|
+
*
|
|
8
|
+
* Fields marked `.volatile()` become separately addressable live references
|
|
9
|
+
* (`config.enabled.get()`). Only volatile fields appear in the settings form
|
|
10
|
+
* (`dsh-settings`'s `volatileForm()`), and an entry whose Config has NO
|
|
11
|
+
* volatile field is dropped from the form list entirely — so the set of
|
|
12
|
+
* `.volatile()` marks below IS the settings UI's field list.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-llm-multi-rate-limiter/types/config
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-model overrides — key is `"provider/model"` (e.g. `"deepseek/deepseek-chat"`).
|
|
21
|
+
* Every field is optional; omitted fields inherit from `defaults`.
|
|
22
|
+
*
|
|
23
|
+
* This sub-schema deliberately carries NO `.volatile()`: it is the `inner`
|
|
24
|
+
* of a `dict`, and two volatile layers on one path are rejected
|
|
25
|
+
* (`volatile fields require a fixed object path without an enclosing volatile
|
|
26
|
+
* field`, `schemastery/lib/index.mjs:246`). The whole `models` dict is made
|
|
27
|
+
* volatile instead — see below.
|
|
28
|
+
*/
|
|
29
|
+
const ModelOverride = Schema.object({
|
|
30
|
+
maxConcurrent: Schema.number().step(1).min(1).description("Max concurrent requests for this model"),
|
|
31
|
+
maxRpm: Schema.number().step(1).min(1).description("Max requests per minute for this model"),
|
|
32
|
+
burstSize: Schema.number().step(1).min(1).description("Burst capacity for this model (token-bucket)"),
|
|
33
|
+
refillRate: Schema.number().min(0.1).description("Tokens refilled per second (token-bucket)"),
|
|
34
|
+
enabled: Schema.boolean().description("Enable/disable rate limiting for this model"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-provider overrides — key is the provider id (e.g. `"openai"`).
|
|
39
|
+
*
|
|
40
|
+
* Sits between the global `defaults` and the per-model tier: one provider's
|
|
41
|
+
* models usually share a single quota, so this is where that quota belongs.
|
|
42
|
+
* Wildcards are accepted here too (`"mb-int-*"`).
|
|
43
|
+
*/
|
|
44
|
+
const ProviderOverride = Schema.object({
|
|
45
|
+
maxConcurrent: Schema.number().step(1).min(1).description("Max concurrent requests for this provider"),
|
|
46
|
+
maxRpm: Schema.number().step(1).min(1).description("Max requests per minute for this provider"),
|
|
47
|
+
burstSize: Schema.number().step(1).min(1).description("Burst capacity for this provider (token-bucket)"),
|
|
48
|
+
refillRate: Schema.number().min(0.1).description("Tokens refilled per second (token-bucket)"),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-model defaults — applied to every model unless overridden.
|
|
53
|
+
* The object is volatile AS A WHOLE so `config.defaults.get()` yields one
|
|
54
|
+
* plain object and the settings form edits `["defaults", field]` paths.
|
|
55
|
+
*/
|
|
56
|
+
const Defaults = Schema.object({
|
|
57
|
+
maxConcurrent: Schema.number().step(1).min(1).default(5).description("Max concurrent requests"),
|
|
58
|
+
maxRpm: Schema.number().step(1).min(1).default(60).description("Max requests per minute"),
|
|
59
|
+
burstSize: Schema.number().step(1).min(1).default(10).description("Burst capacity (token-bucket)"),
|
|
60
|
+
refillRate: Schema.number().min(0.1).default(1).description("Tokens per second (token-bucket)"),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adaptive back-off after the provider answers 429.
|
|
65
|
+
*
|
|
66
|
+
* The limiter cannot see the provider's own quota arithmetic, so a `RATE_LIMIT`
|
|
67
|
+
* failure is treated as a hint: the model's rate is scaled by `factor` for
|
|
68
|
+
* `cooldownMs`, then restored after `recoverAfter` clean calls.
|
|
69
|
+
*/
|
|
70
|
+
const Cooldown = Schema.object({
|
|
71
|
+
enabled: Schema.boolean().default(true).description("Slow a model down after the provider returns 429"),
|
|
72
|
+
cooldownMs: Schema.number().step(1).min(1000).default(30_000).description("How long the reduced rate lasts"),
|
|
73
|
+
factor: Schema.number().min(0.05).max(0.9).step(0.05).default(0.5).description("Rate multiplier while cooling"),
|
|
74
|
+
recoverAfter: Schema.number().step(1).min(1).default(3).description("Clean calls before the full rate returns"),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const Config = Schema.object({
|
|
78
|
+
/** Master switch */
|
|
79
|
+
enabled: Schema.boolean().default(true).volatile().description("Enable the rate limiter globally"),
|
|
80
|
+
|
|
81
|
+
/** Default strategy */
|
|
82
|
+
strategy: Schema.union([
|
|
83
|
+
Schema.const("token-bucket").description("Token Bucket — allows bursts up to burstSize"),
|
|
84
|
+
Schema.const("sliding-window").description("Sliding Window — smooth, fixed requests-per-minute"),
|
|
85
|
+
]).default("token-bucket").volatile().description("Rate limiting algorithm"),
|
|
86
|
+
|
|
87
|
+
/** Global defaults — applied to every model unless overridden */
|
|
88
|
+
defaults: Defaults.default({}).volatile().description("Default limits applied to all models"),
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Per-provider overrides — key is the provider id, wildcards allowed.
|
|
92
|
+
*
|
|
93
|
+
* Volatile as a whole block, for the same reason as `models` below: two
|
|
94
|
+
* volatile layers on one path are rejected by schemastery.
|
|
95
|
+
*/
|
|
96
|
+
providers: Schema.dict(ProviderOverride).default({}).volatile()
|
|
97
|
+
.description("Per-provider rate limit overrides"),
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Per-model overrides — key is "provider/model"; `*` matches any run of
|
|
101
|
+
* characters inside one path segment, so `openai/*` covers every OpenAI model.
|
|
102
|
+
*
|
|
103
|
+
* WHOLE-BLOCK volatile: `dict(inner).volatile()`, never
|
|
104
|
+
* `dict(inner.volatile())`. The latter marks the dict's `inner` while the
|
|
105
|
+
* `sKey`/`inner` nodes are "blocked" (`schemastery/lib/index.mjs:249-250`),
|
|
106
|
+
* which throws at validation time.
|
|
107
|
+
*
|
|
108
|
+
* Because `isVolatilePath` (`dsh-settings/lib/index.js:153-158`) returns
|
|
109
|
+
* true on reaching ANY volatile node without inspecting its descendants,
|
|
110
|
+
* paths such as `["models", "provider/model", "maxRpm"]` remain writable
|
|
111
|
+
* through the settings form under this whole-block mark.
|
|
112
|
+
*/
|
|
113
|
+
models: Schema.dict(ModelOverride).default({}).volatile().description("Per-model rate limit overrides"),
|
|
114
|
+
|
|
115
|
+
/** Adaptive back-off after a provider-side 429 */
|
|
116
|
+
cooldown: Cooldown.default({}).volatile().description("Slow down after the provider returns 429"),
|
|
117
|
+
|
|
118
|
+
/** Queue behaviour when a request is throttled */
|
|
119
|
+
onThrottled: Schema.union([
|
|
120
|
+
Schema.const("queue").description("Wait in queue until a slot opens"),
|
|
121
|
+
Schema.const("reject").description("Immediately return an error"),
|
|
122
|
+
]).default("queue").volatile().description("What happens when a request exceeds the limit"),
|
|
123
|
+
|
|
124
|
+
/** Maximum time a request waits in queue before being rejected */
|
|
125
|
+
maxQueueWaitMs: Schema.number().step(1).min(1000).default(60_000).volatile().description("Max queue wait (ms)"),
|
|
126
|
+
}).description("LLM call rate limiter settings");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Config } from "./config.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naced/llm-multi-rate-limiter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Per-model LLM call rate limiter for DeepSeek Harness with a Settings section UI, queueing and a live status panel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20",
|
|
18
|
+
"dsh": ">=0.2.1-rc.2"
|
|
19
|
+
},
|
|
20
|
+
"author": "Naced <869521569@qq.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/NacedWang/dsh-llm-multi-rate-limiter.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/NacedWang/dsh-llm-multi-rate-limiter#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/NacedWang/dsh-llm-multi-rate-limiter/issues"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-connection",
|
|
40
|
+
"@deepseek-ai/dsh-client-locale",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
42
|
+
],
|
|
43
|
+
"platform": "web"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "node --check lib/index.js && node --check lib/client.js && node --check lib/config-resolve.js",
|
|
48
|
+
"test": "node --test",
|
|
49
|
+
"lint": "eslint .",
|
|
50
|
+
"check": "npm run build && npm run lint && npm test",
|
|
51
|
+
"prepublishOnly": "npm run build && npm test"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"dsh",
|
|
55
|
+
"deepseek-harness",
|
|
56
|
+
"rate-limiter",
|
|
57
|
+
"llm",
|
|
58
|
+
"token-bucket",
|
|
59
|
+
"sliding-window",
|
|
60
|
+
"cordis-plugin"
|
|
61
|
+
],
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
64
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1",
|
|
65
|
+
"@deepseek-ai/schemastery": ">=3.18.4"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
|
|
72
|
+
"eslint": "^9.15.0"
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"lib/",
|
|
76
|
+
"cordis.patch.yml",
|
|
77
|
+
"package.json",
|
|
78
|
+
"README.zh.md",
|
|
79
|
+
"README.md",
|
|
80
|
+
"CHANGELOG.md",
|
|
81
|
+
"LICENSE"
|
|
82
|
+
]
|
|
83
|
+
}
|