@goodandready/dsh-key-rotation 0.8.76 → 0.8.78
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/error-taxonomy.js +59 -10
- package/lib/errors.js +302 -0
- package/lib/index.js +6 -0
- package/lib/model-catalog.js +296 -0
- package/lib/pool-selection.js +126 -28
- package/lib/pool.js +19 -0
- package/lib/quota-runway.js +222 -0
- package/lib/rotate.js +61 -11
- package/lib/session-router.js +265 -0
- 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 +5 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// lib/transports/anthropic.js — Anthropic Claude Messages wire protocol parsing (#546).
|
|
2
|
+
// Handles event discrimination, text/thinking deltas, and split-event usage accumulation.
|
|
3
|
+
|
|
4
|
+
import { normalizeUsageTokens } from './tokens.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses an Anthropic event payload.
|
|
8
|
+
* @param {string|object} data
|
|
9
|
+
* @returns {object|null}
|
|
10
|
+
*/
|
|
11
|
+
export function parseAnthropicEvent(data) {
|
|
12
|
+
if (!data) return null;
|
|
13
|
+
if (typeof data === 'object') return data;
|
|
14
|
+
if (typeof data !== 'string') return null;
|
|
15
|
+
|
|
16
|
+
const trimmed = data.trim();
|
|
17
|
+
if (!trimmed) return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(trimmed);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts content or reasoning delta from an Anthropic event.
|
|
28
|
+
* @param {object} event
|
|
29
|
+
* @returns {{
|
|
30
|
+
* type: string,
|
|
31
|
+
* text: string|null,
|
|
32
|
+
* thinking: string|null,
|
|
33
|
+
* partialJson: string|null,
|
|
34
|
+
* stopReason: string|null
|
|
35
|
+
* }}
|
|
36
|
+
*/
|
|
37
|
+
export function extractAnthropicDelta(event) {
|
|
38
|
+
if (!event || typeof event !== 'object') {
|
|
39
|
+
return { type: 'unknown', text: null, thinking: null, partialJson: null, stopReason: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const type = event.type || 'unknown';
|
|
43
|
+
|
|
44
|
+
if (type === 'content_block_delta') {
|
|
45
|
+
const delta = event.delta || {};
|
|
46
|
+
const deltaType = delta.type;
|
|
47
|
+
return {
|
|
48
|
+
type,
|
|
49
|
+
text: deltaType === 'text_delta' && typeof delta.text === 'string' ? delta.text : null,
|
|
50
|
+
thinking: deltaType === 'thinking_delta' && typeof delta.thinking === 'string' ? delta.thinking : null,
|
|
51
|
+
partialJson: deltaType === 'input_json_delta' && typeof delta.partial_json === 'string' ? delta.partial_json : null,
|
|
52
|
+
stopReason: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (type === 'message_delta') {
|
|
57
|
+
return {
|
|
58
|
+
type,
|
|
59
|
+
text: null,
|
|
60
|
+
thinking: null,
|
|
61
|
+
partialJson: null,
|
|
62
|
+
stopReason: event.delta?.stop_reason ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { type, text: null, thinking: null, partialJson: null, stopReason: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates an accumulator for Anthropic streaming usage.
|
|
71
|
+
* Anthropic provides input tokens at `message_start` and output tokens at `message_delta`.
|
|
72
|
+
*
|
|
73
|
+
* @returns {{
|
|
74
|
+
* accumulate: (event: object) => void,
|
|
75
|
+
* getUsage: () => object,
|
|
76
|
+
* reset: () => void
|
|
77
|
+
* }}
|
|
78
|
+
*/
|
|
79
|
+
export function createAnthropicUsageAccumulator() {
|
|
80
|
+
let inputTokens = 0;
|
|
81
|
+
let outputTokens = 0;
|
|
82
|
+
let cacheReadTokens = 0;
|
|
83
|
+
let cacheWriteTokens = 0;
|
|
84
|
+
let rawStart = null;
|
|
85
|
+
let rawDelta = null;
|
|
86
|
+
|
|
87
|
+
function accumulate(event) {
|
|
88
|
+
if (!event || typeof event !== 'object') return;
|
|
89
|
+
|
|
90
|
+
if (event.type === 'message_start' && event.message?.usage) {
|
|
91
|
+
rawStart = event.message.usage;
|
|
92
|
+
const norm = normalizeUsageTokens(rawStart);
|
|
93
|
+
inputTokens = norm.inputTokens;
|
|
94
|
+
cacheReadTokens = norm.cacheReadTokens;
|
|
95
|
+
cacheWriteTokens = norm.cacheWriteTokens;
|
|
96
|
+
} else if (event.type === 'message_delta' && event.usage) {
|
|
97
|
+
rawDelta = event.usage;
|
|
98
|
+
const norm = normalizeUsageTokens(rawDelta);
|
|
99
|
+
outputTokens = norm.outputTokens;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getUsage() {
|
|
104
|
+
const total = inputTokens + outputTokens;
|
|
105
|
+
return {
|
|
106
|
+
inputTokens,
|
|
107
|
+
outputTokens,
|
|
108
|
+
cacheReadTokens,
|
|
109
|
+
cacheWriteTokens,
|
|
110
|
+
totalTokens: total,
|
|
111
|
+
raw: { start: rawStart, delta: rawDelta },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function reset() {
|
|
116
|
+
inputTokens = 0;
|
|
117
|
+
outputTokens = 0;
|
|
118
|
+
cacheReadTokens = 0;
|
|
119
|
+
cacheWriteTokens = 0;
|
|
120
|
+
rawStart = null;
|
|
121
|
+
rawDelta = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { accumulate, getUsage, reset };
|
|
125
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// lib/transports/index.js — Unified network transports and token normalization layer (#546).
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
normalizeUsageTokens,
|
|
5
|
+
extractTotalTokens,
|
|
6
|
+
} from './tokens.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
createSseDecoder,
|
|
10
|
+
parseSseStream,
|
|
11
|
+
} from './sse.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
parseOpenAiChunk,
|
|
15
|
+
extractOpenAiDelta,
|
|
16
|
+
extractOpenAiUsage,
|
|
17
|
+
} from './openai.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
parseAnthropicEvent,
|
|
21
|
+
extractAnthropicDelta,
|
|
22
|
+
createAnthropicUsageAccumulator,
|
|
23
|
+
} from './anthropic.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// lib/transports/openai.js — OpenAI wire protocol parsing (#546).
|
|
2
|
+
// Handles JSON deltas, reasoning streams, tool calls, and usage payloads.
|
|
3
|
+
|
|
4
|
+
import { normalizeUsageTokens } from './tokens.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses an OpenAI streaming JSON data chunk.
|
|
8
|
+
* @param {string|object} data
|
|
9
|
+
* @returns {object|null}
|
|
10
|
+
*/
|
|
11
|
+
export function parseOpenAiChunk(data) {
|
|
12
|
+
if (!data) return null;
|
|
13
|
+
if (typeof data === 'object') return data;
|
|
14
|
+
if (typeof data !== 'string') return null;
|
|
15
|
+
|
|
16
|
+
const trimmed = data.trim();
|
|
17
|
+
if (!trimmed || trimmed === '[DONE]') return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(trimmed);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts content delta, reasoning delta, and tool calls from an OpenAI chunk.
|
|
28
|
+
* @param {object} chunk
|
|
29
|
+
* @returns {{
|
|
30
|
+
* content: string|null,
|
|
31
|
+
* reasoningContent: string|null,
|
|
32
|
+
* toolCalls: Array<object>|null,
|
|
33
|
+
* finishReason: string|null
|
|
34
|
+
* }}
|
|
35
|
+
*/
|
|
36
|
+
export function extractOpenAiDelta(chunk) {
|
|
37
|
+
if (!chunk || typeof chunk !== 'object') {
|
|
38
|
+
return {
|
|
39
|
+
content: null,
|
|
40
|
+
reasoningContent: null,
|
|
41
|
+
toolCalls: null,
|
|
42
|
+
finishReason: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : null;
|
|
47
|
+
if (!choice) {
|
|
48
|
+
return {
|
|
49
|
+
content: null,
|
|
50
|
+
reasoningContent: null,
|
|
51
|
+
toolCalls: null,
|
|
52
|
+
finishReason: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const delta = choice.delta ?? {};
|
|
57
|
+
const finishReason = choice.finish_reason ?? null;
|
|
58
|
+
|
|
59
|
+
const content = typeof delta.content === 'string' ? delta.content : null;
|
|
60
|
+
|
|
61
|
+
// Support reasoning fields: DeepSeek reasoning_content, OpenAI thought/reasoning
|
|
62
|
+
const reasoningContent =
|
|
63
|
+
typeof delta.reasoning_content === 'string'
|
|
64
|
+
? delta.reasoning_content
|
|
65
|
+
: typeof delta.reasoning === 'string'
|
|
66
|
+
? delta.reasoning
|
|
67
|
+
: typeof delta.thought === 'string'
|
|
68
|
+
? delta.thought
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
const toolCalls = Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0
|
|
72
|
+
? delta.tool_calls
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content,
|
|
77
|
+
reasoningContent,
|
|
78
|
+
toolCalls,
|
|
79
|
+
finishReason,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extracts and normalizes usage from an OpenAI chunk (e.g. stream_options.include_usage).
|
|
85
|
+
* @param {object} chunk
|
|
86
|
+
* @returns {object|null}
|
|
87
|
+
*/
|
|
88
|
+
export function extractOpenAiUsage(chunk) {
|
|
89
|
+
if (!chunk || typeof chunk !== 'object') return null;
|
|
90
|
+
const usage = chunk.usage;
|
|
91
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
92
|
+
return normalizeUsageTokens(usage);
|
|
93
|
+
}
|
|
@@ -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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.78",
|
|
4
4
|
"packageManager": "pnpm@10.33.2",
|
|
5
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.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
"exports": {
|
|
27
27
|
".": "./lib/index.js",
|
|
28
28
|
"./client": "./lib/client.js",
|
|
29
|
+
"./transports": "./lib/transports/index.js",
|
|
30
|
+
"./catalog": "./lib/model-catalog.js",
|
|
31
|
+
"./runway": "./lib/quota-runway.js",
|
|
29
32
|
"./package.json": "./package.json",
|
|
30
33
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
31
34
|
},
|
|
32
35
|
"files": [
|
|
36
|
+
"lib/**/*.js",
|
|
33
37
|
"lib/*.js",
|
|
34
38
|
"cordis.patch.yml",
|
|
35
39
|
"README.md",
|