@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.
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }