@goodandready/dsh-key-rotation 0.7.30 → 0.7.32
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/LICENSE +21 -21
- package/README.md +254 -172
- package/README.ru.md +254 -0
- package/README.zh.md +215 -0
- package/cordis.patch.yml +6 -6
- package/lib/agent-budget.js +68 -68
- package/lib/bucket.js +129 -52
- package/lib/canary.js +63 -56
- package/lib/client-helpers.js +21 -21
- package/lib/client.js +1156 -1063
- package/lib/concurrency.js +73 -72
- package/lib/heal.js +35 -35
- package/lib/histogram.js +66 -66
- package/lib/incident.js +76 -76
- package/lib/index.js +1850 -1836
- package/lib/pool.js +264 -237
- package/lib/quota-window.js +45 -45
- package/lib/quota.js +39 -39
- package/lib/region.js +50 -50
- package/lib/sandbox.js +117 -117
- package/lib/shadow.js +81 -81
- package/lib/usage-report.js +79 -49
- package/lib/webhook.js +193 -133
- package/package.json +58 -58
package/lib/usage-report.js
CHANGED
|
@@ -1,49 +1,79 @@
|
|
|
1
|
-
// lib/usage-report.js - #209 usage report rows from pool state. Pure helpers.
|
|
2
|
-
const DAY_MS = 86400000;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
// lib/usage-report.js - #209 usage report rows from pool state + 30-day compaction (#11). Pure helpers.
|
|
2
|
+
const DAY_MS = 86400000;
|
|
3
|
+
export const MAX_USAGE_RETENTION_DAYS = 30;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build per-key usage rows for the last `days` ISO days (default 7).
|
|
7
|
+
* Returns [{ ref, requests, cost, active, usageByDay: {day: n} }].
|
|
8
|
+
*/
|
|
9
|
+
export function usageRows(pool, days = 7, now = Date.now()) {
|
|
10
|
+
const out = [];
|
|
11
|
+
const dayKeys = [];
|
|
12
|
+
for (let i = 0; i < Math.max(1, days); i++) dayKeys.push(new Date(now - i * DAY_MS).toISOString().slice(0, 10));
|
|
13
|
+
const refs = pool?.refs ?? [];
|
|
14
|
+
for (const ref of refs) {
|
|
15
|
+
const daysMap = pool.state.usageDays?.get(ref) ?? new Map();
|
|
16
|
+
const costMap = pool.state.costDays?.get(ref) ?? new Map();
|
|
17
|
+
let requests = 0, cost = 0;
|
|
18
|
+
const usageByDay = {};
|
|
19
|
+
for (const d of dayKeys) {
|
|
20
|
+
const r = daysMap.get(d) ?? 0;
|
|
21
|
+
const c = costMap.get(d) ?? 0;
|
|
22
|
+
requests += r;
|
|
23
|
+
cost += c;
|
|
24
|
+
usageByDay[d] = r;
|
|
25
|
+
}
|
|
26
|
+
out.push({
|
|
27
|
+
ref,
|
|
28
|
+
requests,
|
|
29
|
+
cost: Math.round(cost * 100) / 100,
|
|
30
|
+
active: pool.state.lastUsed === ref,
|
|
31
|
+
usageByDay,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** CSV of usage rows: ref,requests,cost,active + per-day columns. */
|
|
38
|
+
export function usageCsv(rows) {
|
|
39
|
+
const dayCols = [...new Set(rows.flatMap((r) => Object.keys(r.usageByDay)))].sort();
|
|
40
|
+
const head = ['ref', 'requests', 'cost', 'active', ...dayCols];
|
|
41
|
+
const esc = (v) => {
|
|
42
|
+
const s = String(v ?? '');
|
|
43
|
+
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
44
|
+
};
|
|
45
|
+
const lines = [head.join(',')];
|
|
46
|
+
for (const r of rows) {
|
|
47
|
+
lines.push([esc(r.ref), r.requests, r.cost, r.active ? 'yes' : 'no', ...dayCols.map((d) => r.usageByDay[d] ?? 0)].join(','));
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compact usage days/cost data by purging dates older than maxDays (default 30).
|
|
54
|
+
* Returns count of purged day keys.
|
|
55
|
+
*/
|
|
56
|
+
export function compactUsage(pool, maxDays = MAX_USAGE_RETENTION_DAYS, now = Date.now()) {
|
|
57
|
+
if (!pool?.state) return 0;
|
|
58
|
+
const cutoffIso = new Date(now - maxDays * DAY_MS).toISOString().slice(0, 10);
|
|
59
|
+
let purged = 0;
|
|
60
|
+
|
|
61
|
+
for (const daysMap of pool.state.usageDays?.values() ?? []) {
|
|
62
|
+
for (const d of [...daysMap.keys()]) {
|
|
63
|
+
if (d < cutoffIso) {
|
|
64
|
+
daysMap.delete(d);
|
|
65
|
+
purged++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const costMap of pool.state.costDays?.values() ?? []) {
|
|
71
|
+
for (const d of [...costMap.keys()]) {
|
|
72
|
+
if (d < cutoffIso) {
|
|
73
|
+
costMap.delete(d);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return purged;
|
|
79
|
+
}
|
package/lib/webhook.js
CHANGED
|
@@ -1,133 +1,193 @@
|
|
|
1
|
-
// lib/webhook.js — webhook sender with throttle
|
|
2
|
-
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (u.includes('api
|
|
13
|
-
if (u.includes('
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
type: '
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
? JSON.stringify(
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const out =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
// lib/webhook.js — webhook sender with throttle, interactive actions + alert digest debouncer (#199, #10).
|
|
2
|
+
export const WEBHOOK_TIMEOUT_MS = 5000;
|
|
3
|
+
export const WEBHOOK_MIN_INTERVAL_MS = 1000;
|
|
4
|
+
export const WEBHOOK_RETRY_DELAY_MS = 2000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect platform from webhook URL (#199).
|
|
8
|
+
*/
|
|
9
|
+
export function detectPlatform(url) {
|
|
10
|
+
const u = String(url ?? '');
|
|
11
|
+
if (u.includes('api.telegram.org')) return 'telegram';
|
|
12
|
+
if (u.includes('discord.com/api/webhooks')) return 'discord';
|
|
13
|
+
if (u.includes('hooks.slack.com')) return 'slack';
|
|
14
|
+
return 'generic';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a platform-specific interactive body (#199).
|
|
19
|
+
* payload: { title, text, actions: [{ id, label }] }
|
|
20
|
+
*/
|
|
21
|
+
export function formatInteractive(url, payload, actionToken) {
|
|
22
|
+
const platform = detectPlatform(url);
|
|
23
|
+
const actions = Array.isArray(payload.actions) ? payload.actions : [];
|
|
24
|
+
const title = String(payload.title ?? 'dsh-key-rotation');
|
|
25
|
+
const text = String(payload.text ?? '');
|
|
26
|
+
if (platform === 'telegram') {
|
|
27
|
+
return {
|
|
28
|
+
text: `*${title}*\n${text}`,
|
|
29
|
+
parse_mode: 'Markdown',
|
|
30
|
+
reply_markup: {
|
|
31
|
+
inline_keyboard: [actions.map((a) => ({
|
|
32
|
+
text: a.label,
|
|
33
|
+
callback_data: JSON.stringify({ id: a.id, token: actionToken }),
|
|
34
|
+
}))],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'discord') {
|
|
39
|
+
return {
|
|
40
|
+
content: `**${title}**\n${text}`,
|
|
41
|
+
components: [{
|
|
42
|
+
type: 1, // action row
|
|
43
|
+
components: actions.map((a) => ({
|
|
44
|
+
type: 2, // button
|
|
45
|
+
style: 4, // danger
|
|
46
|
+
label: String(a.label).slice(0, 80),
|
|
47
|
+
custom_id: JSON.stringify({ id: a.id, token: actionToken }),
|
|
48
|
+
})),
|
|
49
|
+
}],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (platform === 'slack') {
|
|
53
|
+
return {
|
|
54
|
+
text: `*${title}*\n${text}`,
|
|
55
|
+
blocks: [
|
|
56
|
+
{ type: 'section', text: { type: 'mrkdwn', text: `*${title}*\n${text}` } },
|
|
57
|
+
{
|
|
58
|
+
type: 'actions',
|
|
59
|
+
elements: actions.map((a) => ({
|
|
60
|
+
type: 'button',
|
|
61
|
+
text: { type: 'plain_text', text: String(a.label).slice(0, 75) },
|
|
62
|
+
value: JSON.stringify({ id: a.id, token: actionToken }),
|
|
63
|
+
})),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// generic JSON webhook: actions as plain data, receiver decides
|
|
69
|
+
return { ...payload, actions };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class WebhookSender {
|
|
73
|
+
constructor({ fetchImpl, minIntervalMs = WEBHOOK_MIN_INTERVAL_MS, timeoutMs = WEBHOOK_TIMEOUT_MS } = {}) {
|
|
74
|
+
if (typeof fetchImpl !== 'function') throw new Error('webhook: fetchImpl required');
|
|
75
|
+
this._fetch = fetchImpl;
|
|
76
|
+
this._minIntervalMs = minIntervalMs;
|
|
77
|
+
this._timeoutMs = timeoutMs;
|
|
78
|
+
this._lastSentAt = new Map();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async send(url, payload, now = Date.now()) {
|
|
82
|
+
if (!url || typeof url !== 'string') return { sent: false };
|
|
83
|
+
const last = this._lastSentAt.get(url);
|
|
84
|
+
if (Number.isFinite(last) && now - last < this._minIntervalMs) return { sent: false, throttled: true };
|
|
85
|
+
// #199: interactive payload -> platform-specific buttons with callback data
|
|
86
|
+
const body = (payload && typeof payload === 'object' && Array.isArray(payload.actions) && payload.actions.length > 0)
|
|
87
|
+
? JSON.stringify(formatInteractive(url, payload, payload.actionToken))
|
|
88
|
+
: (typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
89
|
+
const ctrl = new AbortController();
|
|
90
|
+
const timer = setTimeout(() => ctrl.abort(), this._timeoutMs);
|
|
91
|
+
const doFetch = () => this._fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body,
|
|
95
|
+
signal: ctrl.signal,
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await doFetch();
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e && e.name === 'AbortError') return { sent: false, error: 'timeout' };
|
|
103
|
+
return { sent: false, error: 'network' };
|
|
104
|
+
}
|
|
105
|
+
if (res.status >= 500 && res.status < 600) {
|
|
106
|
+
await new Promise((r) => setTimeout(r, WEBHOOK_RETRY_DELAY_MS));
|
|
107
|
+
if (ctrl.signal.aborted) return { sent: false, error: 'timeout' };
|
|
108
|
+
try {
|
|
109
|
+
res = await doFetch();
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return { sent: false, error: 'network' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Only mark on success — failures shouldn't block future sends.
|
|
115
|
+
if (res.ok) this._lastSentAt.set(url, now);
|
|
116
|
+
return { sent: res.ok, status: res.status };
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
reset(url) {
|
|
123
|
+
if (url) this._lastSentAt.delete(url);
|
|
124
|
+
else this._lastSentAt.clear();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
snapshot() {
|
|
128
|
+
const out = {};
|
|
129
|
+
for (const [k, v] of this._lastSentAt) out[k] = v;
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* AlertDebouncer aggregates rapid alert events across providers into a consolidated digest.
|
|
136
|
+
*/
|
|
137
|
+
export class AlertDebouncer {
|
|
138
|
+
constructor({ sender, debounceMs = 5000, maxBatch = 10 } = {}) {
|
|
139
|
+
this._sender = sender;
|
|
140
|
+
this._debounceMs = debounceMs;
|
|
141
|
+
this._maxBatch = maxBatch;
|
|
142
|
+
this._pending = new Map(); // url -> { timer, events: [], callbacks: [] }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
enqueue(url, event, onFlushed = null) {
|
|
146
|
+
if (!url) return;
|
|
147
|
+
let entry = this._pending.get(url);
|
|
148
|
+
if (!entry) {
|
|
149
|
+
entry = { events: [], timer: null, callbacks: [] };
|
|
150
|
+
this._pending.set(url, entry);
|
|
151
|
+
}
|
|
152
|
+
entry.events.push(event);
|
|
153
|
+
if (onFlushed) entry.callbacks.push(onFlushed);
|
|
154
|
+
|
|
155
|
+
if (entry.events.length >= this._maxBatch) {
|
|
156
|
+
return this.flush(url);
|
|
157
|
+
}
|
|
158
|
+
if (!entry.timer) {
|
|
159
|
+
entry.timer = setTimeout(() => this.flush(url), this._debounceMs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async flush(url) {
|
|
164
|
+
const entry = this._pending.get(url);
|
|
165
|
+
if (!entry) return;
|
|
166
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
167
|
+
this._pending.delete(url);
|
|
168
|
+
|
|
169
|
+
const events = entry.events;
|
|
170
|
+
if (events.length === 0) return;
|
|
171
|
+
|
|
172
|
+
let payload;
|
|
173
|
+
if (events.length === 1) {
|
|
174
|
+
payload = events[0];
|
|
175
|
+
} else {
|
|
176
|
+
const providers = [...new Set(events.map((e) => e.provider).filter(Boolean))];
|
|
177
|
+
const keys = [...new Set(events.flatMap((e) => e.keys || [e.key]).filter(Boolean))];
|
|
178
|
+
payload = {
|
|
179
|
+
title: `dsh-key-rotation: Alert Digest (${events.length} incidents)`,
|
|
180
|
+
text: `Multiple key incidents:\n- Providers: ${providers.join(', ')}\n- Affected keys: ${keys.join(', ')}`,
|
|
181
|
+
digest: true,
|
|
182
|
+
incidentCount: events.length,
|
|
183
|
+
events,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const res = await this._sender.send(url, payload);
|
|
188
|
+
for (const cb of entry.callbacks) {
|
|
189
|
+
try { cb(res); } catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
return res;
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"deepseek-harness",
|
|
7
|
-
"dsh",
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"bundle",
|
|
10
|
-
"api-key",
|
|
11
|
-
"rotation",
|
|
12
|
-
"quota",
|
|
13
|
-
"rate-limit"
|
|
14
|
-
],
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation.git"
|
|
18
|
-
},
|
|
19
|
-
"homepage": "https://github.com/GooDAnDReaDY/dsh-key-rotation",
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation/issues"
|
|
22
|
-
},
|
|
23
|
-
"type": "module",
|
|
24
|
-
"main": "lib/index.js",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": "./lib/index.js",
|
|
27
|
-
"./client": "./lib/client.js",
|
|
28
|
-
"./package.json": "./package.json",
|
|
29
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"lib",
|
|
33
|
-
"cordis.patch.yml",
|
|
34
|
-
"README.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"dsh": {
|
|
38
|
-
"bundle": {
|
|
39
|
-
"patch": "./cordis.patch.yml"
|
|
40
|
-
},
|
|
41
|
-
"client": {
|
|
42
|
-
"platform": "web",
|
|
43
|
-
"inject": [
|
|
44
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"license": "MIT",
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
53
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"test": "node --test test/*.test.js test/*.test.mjs"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
+
"version": "0.7.32",
|
|
4
|
+
"description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"bundle",
|
|
10
|
+
"api-key",
|
|
11
|
+
"rotation",
|
|
12
|
+
"quota",
|
|
13
|
+
"rate-limit"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-key-rotation",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/index.js",
|
|
27
|
+
"./client": "./lib/client.js",
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib",
|
|
33
|
+
"cordis.patch.yml",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"platform": "web",
|
|
43
|
+
"inject": [
|
|
44
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
53
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "node --test test/*.test.js test/*.test.mjs"
|
|
57
|
+
}
|
|
58
|
+
}
|