@goodandready/dsh-key-rotation 0.8.77 → 0.8.79
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 +14 -0
- package/lib/client-src/00-header.js +40 -0
- package/lib/client-src/10-locales.js +585 -0
- package/lib/client-src/20-hooks.js +55 -0
- package/lib/client-src/30-utils.js +185 -0
- package/lib/client-src/40-components.js +346 -0
- package/lib/client-src/50-card-main.js +647 -0
- package/lib/client-src/51-card-pool-mutators.js +501 -0
- package/lib/client-src/52-card-pool-rows.js +590 -0
- package/lib/client-src/53-card-dialogs-and-render.js +568 -0
- package/lib/client-src/60-header-chip.js +137 -0
- package/lib/client-src/90-apply.js +69 -0
- package/lib/config-compat.js +18 -1
- package/lib/http-bridge.js +2 -0
- package/lib/index.js +7 -1
- package/lib/key-state-machine.js +267 -0
- package/lib/model-catalog.js +296 -0
- package/lib/ops-balance.js +2 -147
- package/lib/ops-config-io.js +2 -456
- package/lib/ops-keys.js +2 -567
- package/lib/ops-metrics.js +2 -92
- package/lib/ops-paths.js +2 -24
- package/lib/ops-quick-action.js +2 -168
- package/lib/ops-status.js +2 -269
- package/lib/ops-telemetry.js +2 -276
- package/lib/ops-test.js +2 -255
- package/lib/ops-webhook.js +2 -218
- package/lib/pool-selection.js +36 -13
- package/lib/pool-state.js +3 -0
- package/lib/pool.js +10 -15
- package/lib/quota-runway.js +222 -0
- package/lib/routes/balance.js +147 -0
- package/lib/routes/config.js +456 -0
- package/lib/routes/guard.js +13 -0
- package/lib/routes/index.js +43 -0
- package/lib/routes/keys.js +567 -0
- package/lib/routes/metrics.js +92 -0
- package/lib/routes/paths.js +24 -0
- package/lib/routes/quick-action.js +168 -0
- package/lib/routes/status.js +269 -0
- package/lib/routes/telemetry.js +276 -0
- package/lib/routes/testing.js +255 -0
- package/lib/routes/webhook.js +218 -0
- package/lib/routes-ops.js +2 -29
- package/lib/stream-metrics.js +13 -5
- package/lib/transports/anthropic.js +125 -0
- package/lib/transports/index.js +23 -0
- package/lib/transports/openai.js +93 -0
- package/lib/transports/sse.js +160 -0
- package/lib/transports/tokens.js +124 -0
- package/package.json +15 -10
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// lib/transports/sse.js — Server-Sent Events stream decoder (#546).
|
|
2
|
+
// Zero-dependency streaming parser with buffering, multi-line data support, and AbortSignal awareness.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a stateful SSE stream decoder.
|
|
6
|
+
* @param {object} [options]
|
|
7
|
+
* @param {(event: { event: string, data: string, id?: string, retry?: number }) => void} [options.onEvent]
|
|
8
|
+
* @param {(err: Error) => void} [options.onError]
|
|
9
|
+
* @returns {{ feed: (chunk: string | Uint8Array | Buffer) => void, flush: () => void, reset: () => void }}
|
|
10
|
+
*/
|
|
11
|
+
export function createSseDecoder(options = {}) {
|
|
12
|
+
const { onEvent, onError } = options;
|
|
13
|
+
let buffer = '';
|
|
14
|
+
let eventType = 'message';
|
|
15
|
+
let dataLines = [];
|
|
16
|
+
let eventId = undefined;
|
|
17
|
+
let retry = undefined;
|
|
18
|
+
|
|
19
|
+
function dispatchCurrent() {
|
|
20
|
+
if (dataLines.length === 0) return;
|
|
21
|
+
const dataStr = dataLines.join('\n');
|
|
22
|
+
eventType = eventType || 'message';
|
|
23
|
+
|
|
24
|
+
// Discard standard SSE end-of-stream signal
|
|
25
|
+
if (dataStr.trim() !== '[DONE]' && typeof onEvent === 'function') {
|
|
26
|
+
try {
|
|
27
|
+
onEvent({
|
|
28
|
+
event: eventType,
|
|
29
|
+
data: dataStr,
|
|
30
|
+
id: eventId,
|
|
31
|
+
retry,
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (typeof onError === 'function') onError(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
eventType = 'message';
|
|
39
|
+
dataLines = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function processLine(rawLine) {
|
|
43
|
+
let line = rawLine;
|
|
44
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
45
|
+
|
|
46
|
+
// Empty line signals dispatch of current event buffer
|
|
47
|
+
if (!line) {
|
|
48
|
+
dispatchCurrent();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Comment line: ignored per SSE spec
|
|
53
|
+
if (line.startsWith(':')) return;
|
|
54
|
+
|
|
55
|
+
const colonIdx = line.indexOf(':');
|
|
56
|
+
let field = line;
|
|
57
|
+
let value = '';
|
|
58
|
+
|
|
59
|
+
if (colonIdx !== -1) {
|
|
60
|
+
field = line.slice(0, colonIdx);
|
|
61
|
+
value = line.slice(colonIdx + 1);
|
|
62
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (field === 'event') {
|
|
66
|
+
eventType = value;
|
|
67
|
+
} else if (field === 'data') {
|
|
68
|
+
dataLines.push(value);
|
|
69
|
+
} else if (field === 'id') {
|
|
70
|
+
eventId = value;
|
|
71
|
+
} else if (field === 'retry') {
|
|
72
|
+
const parsed = parseInt(value, 10);
|
|
73
|
+
if (Number.isFinite(parsed) && parsed >= 0) retry = parsed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function feed(chunk) {
|
|
78
|
+
if (chunk == null) return;
|
|
79
|
+
const str = typeof chunk === 'string'
|
|
80
|
+
? chunk
|
|
81
|
+
: (chunk instanceof Uint8Array || Buffer.isBuffer(chunk)
|
|
82
|
+
? Buffer.from(chunk).toString('utf-8')
|
|
83
|
+
: String(chunk));
|
|
84
|
+
|
|
85
|
+
buffer += str;
|
|
86
|
+
let newlineIdx;
|
|
87
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
88
|
+
const line = buffer.slice(0, newlineIdx);
|
|
89
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
90
|
+
processLine(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function flush() {
|
|
95
|
+
if (buffer.length > 0) {
|
|
96
|
+
const line = buffer;
|
|
97
|
+
buffer = '';
|
|
98
|
+
processLine(line);
|
|
99
|
+
}
|
|
100
|
+
dispatchCurrent();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reset() {
|
|
104
|
+
buffer = '';
|
|
105
|
+
eventType = 'message';
|
|
106
|
+
dataLines = [];
|
|
107
|
+
eventId = undefined;
|
|
108
|
+
retry = undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { feed, flush, reset };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Async generator that consumes an async iterable of chunks and yields parsed SSE event objects.
|
|
116
|
+
* Handles AbortSignal cleanly.
|
|
117
|
+
*
|
|
118
|
+
* @param {AsyncIterable<Uint8Array | string>} asyncIterable
|
|
119
|
+
* @param {AbortSignal} [signal]
|
|
120
|
+
* @returns {AsyncGenerator<{ event: string, data: string, id?: string, retry?: number }>}
|
|
121
|
+
*/
|
|
122
|
+
export async function* parseSseStream(asyncIterable, signal) {
|
|
123
|
+
if (!asyncIterable) return;
|
|
124
|
+
|
|
125
|
+
const queue = [];
|
|
126
|
+
let error = null;
|
|
127
|
+
|
|
128
|
+
const decoder = createSseDecoder({
|
|
129
|
+
onEvent: (evt) => queue.push(evt),
|
|
130
|
+
onError: (err) => { error = err; },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const checkAbort = () => {
|
|
134
|
+
if (signal?.aborted) {
|
|
135
|
+
const abortError = new Error(signal.reason?.message ?? 'SSE stream aborted');
|
|
136
|
+
abortError.name = 'AbortError';
|
|
137
|
+
throw abortError;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
for await (const chunk of asyncIterable) {
|
|
143
|
+
checkAbort();
|
|
144
|
+
decoder.feed(chunk);
|
|
145
|
+
if (error) throw error;
|
|
146
|
+
while (queue.length > 0) {
|
|
147
|
+
checkAbort();
|
|
148
|
+
yield queue.shift();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
decoder.flush();
|
|
152
|
+
if (error) throw error;
|
|
153
|
+
while (queue.length > 0) {
|
|
154
|
+
checkAbort();
|
|
155
|
+
yield queue.shift();
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
decoder.reset();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// lib/transports/tokens.js — canonical usage token normalization (#546).
|
|
2
|
+
// Converts diverse provider token usage shapes into canonical DSH token metrics.
|
|
3
|
+
|
|
4
|
+
function toNonNegativeInt(val) {
|
|
5
|
+
if (typeof val === 'number' && Number.isFinite(val) && val >= 0) {
|
|
6
|
+
return Math.floor(val);
|
|
7
|
+
}
|
|
8
|
+
if (typeof val === 'string') {
|
|
9
|
+
const parsed = parseInt(val, 10);
|
|
10
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes provider token usage payloads into canonical DSH shape:
|
|
17
|
+
* {
|
|
18
|
+
* inputTokens: number,
|
|
19
|
+
* outputTokens: number,
|
|
20
|
+
* cacheReadTokens: number,
|
|
21
|
+
* cacheWriteTokens: number,
|
|
22
|
+
* totalTokens: number,
|
|
23
|
+
* raw: object
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Supports OpenAI, Anthropic, Gemini, Ollama, DeepSeek, and generic formats.
|
|
27
|
+
*
|
|
28
|
+
* @param {object|null|undefined} usage Raw usage object from provider
|
|
29
|
+
* @returns {{
|
|
30
|
+
* inputTokens: number,
|
|
31
|
+
* outputTokens: number,
|
|
32
|
+
* cacheReadTokens: number,
|
|
33
|
+
* cacheWriteTokens: number,
|
|
34
|
+
* totalTokens: number,
|
|
35
|
+
* raw: object
|
|
36
|
+
* }}
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeUsageTokens(usage) {
|
|
39
|
+
if (!usage || typeof usage !== 'object') {
|
|
40
|
+
return {
|
|
41
|
+
inputTokens: 0,
|
|
42
|
+
outputTokens: 0,
|
|
43
|
+
cacheReadTokens: 0,
|
|
44
|
+
cacheWriteTokens: 0,
|
|
45
|
+
totalTokens: 0,
|
|
46
|
+
raw: usage ?? {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1. Input / prompt tokens
|
|
51
|
+
const input =
|
|
52
|
+
toNonNegativeInt(usage.inputTokens) ??
|
|
53
|
+
toNonNegativeInt(usage.input_tokens) ??
|
|
54
|
+
toNonNegativeInt(usage.prompt_tokens) ??
|
|
55
|
+
toNonNegativeInt(usage.promptTokenCount) ??
|
|
56
|
+
toNonNegativeInt(usage.prompt_eval_count) ??
|
|
57
|
+
0;
|
|
58
|
+
|
|
59
|
+
// 2. Output / completion tokens
|
|
60
|
+
const output =
|
|
61
|
+
toNonNegativeInt(usage.outputTokens) ??
|
|
62
|
+
toNonNegativeInt(usage.output_tokens) ??
|
|
63
|
+
toNonNegativeInt(usage.completion_tokens) ??
|
|
64
|
+
toNonNegativeInt(usage.candidatesTokenCount) ??
|
|
65
|
+
toNonNegativeInt(usage.eval_count) ??
|
|
66
|
+
0;
|
|
67
|
+
|
|
68
|
+
// 3. Cache read tokens
|
|
69
|
+
const cacheRead =
|
|
70
|
+
toNonNegativeInt(usage.cacheReadTokens) ??
|
|
71
|
+
toNonNegativeInt(usage.cache_read_input_tokens) ??
|
|
72
|
+
toNonNegativeInt(usage.prompt_tokens_details?.cached_tokens) ??
|
|
73
|
+
toNonNegativeInt(usage.cached_tokens) ??
|
|
74
|
+
toNonNegativeInt(usage.prompt_cache_hit_tokens) ??
|
|
75
|
+
toNonNegativeInt(usage.cachedContentTokenCount) ??
|
|
76
|
+
0;
|
|
77
|
+
|
|
78
|
+
// 4. Cache write / creation tokens
|
|
79
|
+
const cacheWrite =
|
|
80
|
+
toNonNegativeInt(usage.cacheWriteTokens) ??
|
|
81
|
+
toNonNegativeInt(usage.cache_creation_input_tokens) ??
|
|
82
|
+
toNonNegativeInt(usage.prompt_tokens_details?.cache_creation_tokens) ??
|
|
83
|
+
toNonNegativeInt(usage.cache_write_tokens) ??
|
|
84
|
+
toNonNegativeInt(usage.prompt_cache_miss_tokens) ??
|
|
85
|
+
0;
|
|
86
|
+
|
|
87
|
+
// 5. Total tokens
|
|
88
|
+
let total =
|
|
89
|
+
toNonNegativeInt(usage.totalTokens) ??
|
|
90
|
+
toNonNegativeInt(usage.total_tokens) ??
|
|
91
|
+
toNonNegativeInt(usage.totalTokenCount);
|
|
92
|
+
|
|
93
|
+
if (total === null) {
|
|
94
|
+
total = input + output;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
inputTokens: input,
|
|
99
|
+
outputTokens: output,
|
|
100
|
+
cacheReadTokens: cacheRead,
|
|
101
|
+
cacheWriteTokens: cacheWrite,
|
|
102
|
+
totalTokens: total,
|
|
103
|
+
raw: usage,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extracts a single total token number for quota and TPM accounting.
|
|
109
|
+
* Returns null if no valid token counts are found.
|
|
110
|
+
* Compatible with model-quota.js extractUsageTokens.
|
|
111
|
+
*
|
|
112
|
+
* @param {object|null|undefined} usage
|
|
113
|
+
* @returns {number|null}
|
|
114
|
+
*/
|
|
115
|
+
export function extractTotalTokens(usage) {
|
|
116
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
117
|
+
|
|
118
|
+
const normalized = normalizeUsageTokens(usage);
|
|
119
|
+
if (normalized.totalTokens > 0) return normalized.totalTokens;
|
|
120
|
+
if (normalized.inputTokens > 0 || normalized.outputTokens > 0) {
|
|
121
|
+
return normalized.inputTokens + normalized.outputTokens;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.79",
|
|
4
|
+
"packageManager": "pnpm@10.33.2",
|
|
4
5
|
"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
6
|
"keywords": [
|
|
6
7
|
"deepseek-harness",
|
|
@@ -25,10 +26,14 @@
|
|
|
25
26
|
"exports": {
|
|
26
27
|
".": "./lib/index.js",
|
|
27
28
|
"./client": "./lib/client.js",
|
|
29
|
+
"./transports": "./lib/transports/index.js",
|
|
30
|
+
"./catalog": "./lib/model-catalog.js",
|
|
31
|
+
"./runway": "./lib/quota-runway.js",
|
|
28
32
|
"./package.json": "./package.json",
|
|
29
33
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
36
|
+
"lib/**/*.js",
|
|
32
37
|
"lib/*.js",
|
|
33
38
|
"cordis.patch.yml",
|
|
34
39
|
"README.md",
|
|
@@ -59,6 +64,13 @@
|
|
|
59
64
|
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || >=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-alpha.1 <0.2.2-0",
|
|
60
65
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
61
66
|
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"test": "node tools/test.mjs",
|
|
69
|
+
"test:core": "node tools/test.mjs --core",
|
|
70
|
+
"check": "node tools/check.mjs",
|
|
71
|
+
"build:client": "node scripts/build-client.mjs",
|
|
72
|
+
"pretest": "node scripts/build-client.mjs"
|
|
73
|
+
},
|
|
62
74
|
"devDependencies": {
|
|
63
75
|
"react": "18.3.1",
|
|
64
76
|
"react-test-renderer": "18.3.1",
|
|
@@ -67,12 +79,5 @@
|
|
|
67
79
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
68
80
|
"semver": "^7.6.3"
|
|
69
81
|
},
|
|
70
|
-
"dependencies": {}
|
|
71
|
-
|
|
72
|
-
"test": "node tools/test.mjs",
|
|
73
|
-
"test:core": "node tools/test.mjs --core",
|
|
74
|
-
"check": "node tools/check.mjs",
|
|
75
|
-
"build:client": "node scripts/build-client.mjs",
|
|
76
|
-
"pretest": "node scripts/build-client.mjs"
|
|
77
|
-
}
|
|
78
|
-
}
|
|
82
|
+
"dependencies": {}
|
|
83
|
+
}
|