@goodandready/dsh-key-rotation 0.8.68 → 0.8.70

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,133 @@
1
+ // lib/stream-metrics.js — streaming success telemetry, usage, cost and token recording (#512).
2
+ // Extracted from lib/rotate.js to enforce < 600 line modularity standard.
3
+
4
+ import { recordSuccess, setSessionSticky, recordFailure, extractRateLimit, isRateLimited } from './pool.js';
5
+ import { recordPromptCache } from './prompt-cache.js';
6
+ import { recordRequestOutcome } from './ops-analytics.js';
7
+ import { tpmRecord } from './bucket.js';
8
+ import { extractUsageTokens, consumeModelTokens } from './model-quota.js';
9
+ import { extractProviderLimits } from './provider-limits-detector.js';
10
+
11
+ export async function* preserveDispatchContext(iterable, storage, store) {
12
+ if (!storage || typeof storage.getStore !== 'function') {
13
+ yield* iterable;
14
+ return;
15
+ }
16
+ const iterator = iterable[Symbol.asyncIterator]();
17
+ const step = (method, arg) => storage.run(store, () => iterator[method](arg));
18
+ try {
19
+ while (true) {
20
+ const next = await step('next');
21
+ if (next.done) return next.value;
22
+ yield next.value;
23
+ }
24
+ } finally {
25
+ if (typeof iterator.return === 'function') await step('return');
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Records success metrics, token windows, local model quota, limits and proactive pause on finished stream chunk.
31
+ * @param {object} pool
32
+ * @param {string} activeRef
33
+ * @param {object} options request options (provider, model, etc.)
34
+ * @param {object} chunk final streaming chunk with usage/metadata
35
+ * @param {object} deps helpers and runtime configuration
36
+ */
37
+ export function recordStreamSuccessMetrics(pool, activeRef, options, chunk, deps) {
38
+ if (!activeRef || !pool?.state) return;
39
+
40
+ const {
41
+ now,
42
+ sessionId,
43
+ attempt,
44
+ runtime0,
45
+ quotaManaged,
46
+ schedulePersist,
47
+ logWarn,
48
+ rawHdrs,
49
+ pushEvent,
50
+ quotaStore,
51
+ } = deps;
52
+
53
+ const nowMs = typeof now === 'function' ? now() : Date.now();
54
+ const wallNow = Date.now();
55
+
56
+ // 1. Record success for active key and session sticky
57
+ recordSuccess(pool, activeRef, nowMs);
58
+ if (sessionId) setSessionSticky(pool, sessionId, activeRef);
59
+
60
+ // 2. Prompt cache and analytics outcome
61
+ const usagePayload = chunk?.usage ?? chunk?.metadata?.usage;
62
+ recordPromptCache(pool, activeRef, options.model, usagePayload, wallNow);
63
+ recordRequestOutcome(pool, {
64
+ ref: activeRef,
65
+ success: true,
66
+ attempts: attempt + 1,
67
+ failovers: attempt,
68
+ cascaded: Boolean(options.__isCascade),
69
+ }, wallNow);
70
+
71
+ // 3. TPM sliding window bucket recording
72
+ const usedTokens = extractUsageTokens(usagePayload);
73
+ if (usedTokens && usedTokens > 0) {
74
+ if (!pool.state.tpmWindows) pool.state.tpmWindows = new Map();
75
+ tpmRecord(pool.state.tpmWindows, activeRef, usedTokens, wallNow);
76
+ }
77
+
78
+ // 4. Local model quota consumption
79
+ if (quotaManaged) {
80
+ const charged = consumeModelTokens(
81
+ pool, activeRef, usedTokens,
82
+ runtime0.quotaResetWindow, wallNow,
83
+ );
84
+ if (charged) {
85
+ if (typeof schedulePersist === 'function') {
86
+ try { schedulePersist(); } catch (_) { /* best-effort persistence */ }
87
+ }
88
+ if (charged.exhausted && typeof logWarn === 'function') {
89
+ logWarn(`[dsh-key-rotation] ${options.provider}/${options.model ?? '-'}: key ${activeRef} reached its local token budget (${charged.used}/${charged.limit}) — excluded from this model only`);
90
+ }
91
+ }
92
+ }
93
+
94
+ // 5. Provider rate limit headers detection (#503)
95
+ if (rawHdrs) {
96
+ const detected = extractProviderLimits(rawHdrs);
97
+ if (detected) {
98
+ if (!pool.state.detectedLimits) pool.state.detectedLimits = new Map();
99
+ pool.state.detectedLimits.set(activeRef, { ...detected, at: nowMs });
100
+ }
101
+ }
102
+
103
+ // 6. Proactive rate-limit guard (#115, #303)
104
+ const guardEnabled = pool.proactiveRateLimitGuard ?? runtime0.proactiveRateLimitGuard ?? true;
105
+ const rate = extractRateLimit(chunk?.metadata?.headers ?? chunk?.headers);
106
+ if (guardEnabled && rate) {
107
+ if (isRateLimited(rate, runtime0.rateLimitThreshold ?? 0.1)) {
108
+ let cool;
109
+ if (typeof rate.retryAfter === 'number' && rate.retryAfter > 0) {
110
+ cool = rate.retryAfter * 1000;
111
+ } else if (rate.reset && rate.reset > nowMs) {
112
+ cool = rate.reset - nowMs;
113
+ } else if (rate.reset && rate.reset > 0 && rate.reset < 86400) {
114
+ cool = Math.round(rate.reset * 1000);
115
+ } else {
116
+ cool = pool.cooldownMs ?? runtime0.cooldownMs;
117
+ }
118
+ const maxCool = pool.maxCooldownMs ?? runtime0.maxCooldownMs;
119
+ const effCool = Math.min(cool, maxCool ?? cool);
120
+ recordFailure(pool, activeRef, nowMs, effCool, maxCool);
121
+ pool.state.proactiveSwitches = (pool.state.proactiveSwitches ?? 0) + 1;
122
+ if (typeof pushEvent === 'function') pushEvent(pool, activeRef, 'RATE_LIMIT', effCool);
123
+ if (typeof logWarn === 'function') {
124
+ logWarn(`[dsh-key-rotation] ${options.provider}: key ${activeRef} proactive pause (remaining ${String(rate.remaining ?? '?')}/${String(rate.limit ?? '?')}, cool ${Math.round(effCool / 1000)}s) — next request will rotate`);
125
+ }
126
+ }
127
+ }
128
+
129
+ // 7. Persist quota snapshot
130
+ if (rate && Number.isFinite(rate.remaining) && quotaStore) {
131
+ quotaStore.set(activeRef, { remaining: rate.remaining, limit: rate.limit, reset: rate.reset, at: nowMs });
132
+ }
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.68",
3
+ "version": "0.8.70",
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": [