dsh-keys-manager 0.0.0 → 0.1.0

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/lib/core.d.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pure key-selection and cooldown policy for the Keys Manager.
3
+ *
4
+ * Nothing here touches Cordis, the filesystem, or a secret. The Host passes
5
+ * plain provider records in and gets the same records back, which keeps the
6
+ * round-robin, sticky-session, cooldown-escalation, and retry decisions
7
+ * unit-testable without a running Harness.
8
+ */
9
+ import type { KeyRecord, KeyStatus, ManagerSettings, ProviderRecord, ProviderStatus } from './domain.js';
10
+ import type { LlmFailureLike } from './host-types.js';
11
+ /** Defaults also exposed in the manager's policy card. */
12
+ export declare const DEFAULT_SETTINGS: ManagerSettings;
13
+ /** Normalize one persisted settings object. */
14
+ export declare function normalizeSettings(value: unknown): ManagerSettings;
15
+ /** Apply and validate a settings patch from the UI; throws on nonsense. */
16
+ export declare function patchSettings(current: ManagerSettings, patch: unknown): ManagerSettings;
17
+ /** Clamp an untyped numeric setting. */
18
+ export declare function clampInteger(value: unknown, min: number, max: number, fallback: number): number;
19
+ /** Whether a key may serve a request now. */
20
+ export declare function isSelectable(key: KeyRecord | undefined, now: number): key is KeyRecord;
21
+ /** Pick the next healthy key for a provider in creation order, advancing the cursor. */
22
+ export declare function pickRoundRobin(provider: ProviderRecord, now: number, exclude?: ReadonlySet<string>): KeyRecord | undefined;
23
+ /** Pick the next healthy key other than `excludeKeyId`, advancing the cursor. */
24
+ export declare function pickHealthyExcluding(provider: ProviderRecord, excludeKeyId: string, now: number): KeyRecord | undefined;
25
+ /** Coerce a persisted cursor into range without mutating it. */
26
+ export declare function normalizeCursor(cursor: number, length: number): number;
27
+ /** Result of marking one key failure. */
28
+ export interface CooldownResult {
29
+ cooldownMs: number;
30
+ level: number;
31
+ cooldownUntil: number;
32
+ }
33
+ /**
34
+ * Record one failed key use and start (or extend) its cooldown.
35
+ *
36
+ * First failure: base cooldown. A failure while cooling, or within the
37
+ * recovery grace window after the previous cooldown expired, doubles the
38
+ * level. A long healthy gap resets the level, so an occasional failure does
39
+ * not inherit ancient escalation. The result is always capped at seven days.
40
+ */
41
+ export declare function applyFailureCooldown(key: KeyRecord, settings: ManagerSettings, now: number, failure: LlmFailureLike | undefined): CooldownResult;
42
+ /**
43
+ * Record one successful use: clear the failure streak, but keep the previous
44
+ * cooldown timestamp/level until the recovery grace window has elapsed. A
45
+ * failure shortly after recovery must still escalate.
46
+ */
47
+ export declare function markKeySuccess(key: KeyRecord, now: number): void;
48
+ /** Record that a request used a key, without changing health. */
49
+ export declare function markKeyUsed(key: KeyRecord, now: number): void;
50
+ /** Status a key row renders. */
51
+ export declare function statusOfKey(key: KeyRecord | undefined, now: number): KeyStatus;
52
+ /** Status a provider row renders. */
53
+ export declare function statusOfProvider(keys: readonly KeyRecord[], now: number): ProviderStatus;
54
+ /** Whether a model-request failure should affect key health at all. */
55
+ export declare function shouldFailover(code: string | undefined): boolean;
56
+ /** Session/provider binding key. */
57
+ export declare function bindingKey(sessionId: string, provider: string): string;
58
+ /** Validate a new key input before it reaches the credential store. */
59
+ export declare function validateKeyInput(alias: unknown, value: unknown): {
60
+ alias: string;
61
+ value: string;
62
+ };
63
+ /** Validate a rename/enable patch. */
64
+ export declare function validateKeyPatch(patch: unknown): {
65
+ alias?: string;
66
+ enabled?: boolean;
67
+ };
68
+ /** Maximum keys per provider (also used by the Host). */
69
+ export declare const KEY_LIMIT = 32;
70
+ /** Re-export the shared update type for callers that only import core. */
71
+ export type { ManagerSettings };
package/lib/core.js ADDED
@@ -0,0 +1,238 @@
1
+ /** Defaults also exposed in the manager's policy card. */
2
+ export const DEFAULT_SETTINGS = {
3
+ retryCount: 2,
4
+ baseCooldownMs: 5 * 60 * 1000,
5
+ recoveryGraceMs: 10 * 60 * 1000,
6
+ maxCooldownMs: 7 * 24 * 60 * 60 * 1000,
7
+ stickySession: true,
8
+ autoSwitch: true,
9
+ };
10
+ const MIN_COOLDOWN_MS = 30 * 1000;
11
+ const MAX_KEYS_PER_PROVIDER = 32;
12
+ const MAX_ALIAS_LENGTH = 80;
13
+ const MAX_KEY_LENGTH = 16 * 1024;
14
+ /** Failure codes that describe the request or configuration, not the key. */
15
+ const NON_KEY_FAILURE_CODES = new Set([
16
+ 'ABORTED',
17
+ 'CANCELLED',
18
+ 'CONTEXT_WINDOW_EXCEEDED',
19
+ 'EMPTY_RESPONSE_REQUEST',
20
+ 'IMAGE_OFFLOAD_REQUIRED',
21
+ 'INVALID_PREPARED_CALL',
22
+ 'INVALID_REQUEST',
23
+ 'NO_ADAPTER',
24
+ 'REGISTRATION_DISPOSED',
25
+ 'UNKNOWN_MODEL',
26
+ 'UNSUPPORTED_OPTION',
27
+ ]);
28
+ /** Normalize one persisted settings object. */
29
+ export function normalizeSettings(value) {
30
+ const source = value !== null && typeof value === 'object' ? value : {};
31
+ return {
32
+ retryCount: clampInteger(source.retryCount, 1, 3, DEFAULT_SETTINGS.retryCount),
33
+ baseCooldownMs: clampInteger(source.baseCooldownMs, MIN_COOLDOWN_MS, DEFAULT_SETTINGS.maxCooldownMs, DEFAULT_SETTINGS.baseCooldownMs),
34
+ recoveryGraceMs: clampInteger(source.recoveryGraceMs, 0, DEFAULT_SETTINGS.maxCooldownMs, DEFAULT_SETTINGS.recoveryGraceMs),
35
+ maxCooldownMs: DEFAULT_SETTINGS.maxCooldownMs,
36
+ stickySession: source.stickySession !== false,
37
+ autoSwitch: source.autoSwitch !== false,
38
+ };
39
+ }
40
+ /** Apply and validate a settings patch from the UI; throws on nonsense. */
41
+ export function patchSettings(current, patch) {
42
+ const source = patch !== null && typeof patch === 'object' ? patch : {};
43
+ if (source.retryCount !== undefined) {
44
+ if (!Number.isInteger(source.retryCount) || source.retryCount < 1 || source.retryCount > 3) {
45
+ throw new TypeError('retryCount must be an integer from 1 to 3');
46
+ }
47
+ }
48
+ if (source.baseCooldownMs !== undefined) {
49
+ if (!Number.isFinite(source.baseCooldownMs) || source.baseCooldownMs < MIN_COOLDOWN_MS) {
50
+ throw new TypeError('baseCooldownMs must be at least 30 seconds');
51
+ }
52
+ }
53
+ if (source.recoveryGraceMs !== undefined) {
54
+ if (!Number.isFinite(source.recoveryGraceMs) || source.recoveryGraceMs < 0) {
55
+ throw new TypeError('recoveryGraceMs must be zero or positive');
56
+ }
57
+ }
58
+ return normalizeSettings({ ...current, ...source });
59
+ }
60
+ /** Clamp an untyped numeric setting. */
61
+ export function clampInteger(value, min, max, fallback) {
62
+ const number = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
63
+ return Math.min(max, Math.max(min, Math.round(number)));
64
+ }
65
+ /** Whether a key may serve a request now. */
66
+ export function isSelectable(key, now) {
67
+ if (key === undefined)
68
+ return false;
69
+ if (key.enabled === false || key.valueMissing === true)
70
+ return false;
71
+ return !(Number(key.cooldownUntil) > now);
72
+ }
73
+ /** Pick the next healthy key for a provider in creation order, advancing the cursor. */
74
+ export function pickRoundRobin(provider, now, exclude) {
75
+ const keys = Array.isArray(provider.keys) ? provider.keys : [];
76
+ if (keys.length === 0)
77
+ return undefined;
78
+ const start = normalizeCursor(provider.cursor, keys.length);
79
+ for (let offset = 0; offset < keys.length; offset += 1) {
80
+ const index = (start + offset) % keys.length;
81
+ const key = keys[index];
82
+ if (exclude?.has(key.id))
83
+ continue;
84
+ if (isSelectable(key, now)) {
85
+ provider.cursor = (index + 1) % keys.length;
86
+ return key;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ /** Pick the next healthy key other than `excludeKeyId`, advancing the cursor. */
92
+ export function pickHealthyExcluding(provider, excludeKeyId, now) {
93
+ const keys = Array.isArray(provider.keys) ? provider.keys : [];
94
+ if (keys.length === 0)
95
+ return undefined;
96
+ const start = normalizeCursor(provider.cursor, keys.length);
97
+ for (let offset = 0; offset < keys.length; offset += 1) {
98
+ const index = (start + offset) % keys.length;
99
+ const key = keys[index];
100
+ if (key.id === excludeKeyId)
101
+ continue;
102
+ if (isSelectable(key, now)) {
103
+ provider.cursor = (index + 1) % keys.length;
104
+ return key;
105
+ }
106
+ }
107
+ return undefined;
108
+ }
109
+ /** Coerce a persisted cursor into range without mutating it. */
110
+ export function normalizeCursor(cursor, length) {
111
+ if (!Number.isInteger(cursor) || length <= 0)
112
+ return 0;
113
+ return ((cursor % length) + length) % length;
114
+ }
115
+ /**
116
+ * Record one failed key use and start (or extend) its cooldown.
117
+ *
118
+ * First failure: base cooldown. A failure while cooling, or within the
119
+ * recovery grace window after the previous cooldown expired, doubles the
120
+ * level. A long healthy gap resets the level, so an occasional failure does
121
+ * not inherit ancient escalation. The result is always capped at seven days.
122
+ */
123
+ export function applyFailureCooldown(key, settings, now, failure) {
124
+ const normalized = normalizeSettings(settings);
125
+ const previousLevel = Number.isInteger(key.failureLevel) ? key.failureLevel : 0;
126
+ const previousUntil = Number(key.cooldownUntil) || 0;
127
+ let level = 0;
128
+ if (previousUntil > 0) {
129
+ if (now <= previousUntil)
130
+ level = previousLevel + 1;
131
+ else if (now - previousUntil <= normalized.recoveryGraceMs)
132
+ level = previousLevel + 1;
133
+ else
134
+ level = 0;
135
+ }
136
+ const providerDelay = Number(failure?.providerRetryAfterMs);
137
+ const base = Math.max(MIN_COOLDOWN_MS, normalized.baseCooldownMs);
138
+ const exponential = Math.min(normalized.maxCooldownMs, base * 2 ** Math.min(level, 30));
139
+ const requested = Number.isFinite(providerDelay) && providerDelay > 0 ? Math.max(exponential, providerDelay) : exponential;
140
+ const cooldownMs = Math.max(1, Math.min(normalized.maxCooldownMs, requested));
141
+ key.failureLevel = level;
142
+ key.cooldownUntil = now + cooldownMs;
143
+ key.lastFailureAt = now;
144
+ key.lastFailureCode = typeof failure?.code === 'string' ? failure.code : '';
145
+ key.consecutiveFailures = (Number(key.consecutiveFailures) || 0) + 1;
146
+ key.totalFailures = (Number(key.totalFailures) || 0) + 1;
147
+ return { cooldownMs, level, cooldownUntil: key.cooldownUntil };
148
+ }
149
+ /**
150
+ * Record one successful use: clear the failure streak, but keep the previous
151
+ * cooldown timestamp/level until the recovery grace window has elapsed. A
152
+ * failure shortly after recovery must still escalate.
153
+ */
154
+ export function markKeySuccess(key, now) {
155
+ key.lastUsedAt = now;
156
+ key.consecutiveFailures = 0;
157
+ key.lastFailureCode = '';
158
+ key.totalUses = (Number(key.totalUses) || 0) + 1;
159
+ }
160
+ /** Record that a request used a key, without changing health. */
161
+ export function markKeyUsed(key, now) {
162
+ key.lastUsedAt = now;
163
+ }
164
+ /** Status a key row renders. */
165
+ export function statusOfKey(key, now) {
166
+ if (key === undefined)
167
+ return 'missing';
168
+ if (key.enabled === false)
169
+ return 'paused';
170
+ if (key.valueMissing === true)
171
+ return 'missing';
172
+ if (Number(key.cooldownUntil) > now)
173
+ return 'cooling';
174
+ return 'active';
175
+ }
176
+ /** Status a provider row renders. */
177
+ export function statusOfProvider(keys, now) {
178
+ if (keys.length === 0)
179
+ return 'empty';
180
+ const statuses = keys.map(key => statusOfKey(key, now));
181
+ if (statuses.includes('active'))
182
+ return 'active';
183
+ if (statuses.includes('cooling'))
184
+ return 'cooling';
185
+ if (statuses.includes('paused'))
186
+ return 'paused';
187
+ return 'missing';
188
+ }
189
+ /** Whether a model-request failure should affect key health at all. */
190
+ export function shouldFailover(code) {
191
+ return typeof code === 'string' && code.length > 0 && !NON_KEY_FAILURE_CODES.has(code);
192
+ }
193
+ /** Session/provider binding key. */
194
+ export function bindingKey(sessionId, provider) {
195
+ return `${sessionId}\u0000${provider}`;
196
+ }
197
+ /** Validate a new key input before it reaches the credential store. */
198
+ export function validateKeyInput(alias, value) {
199
+ const cleanAlias = typeof alias === 'string' ? alias.trim() : '';
200
+ if (cleanAlias.length === 0)
201
+ throw new TypeError('alias is required');
202
+ if (cleanAlias.length > MAX_ALIAS_LENGTH)
203
+ throw new TypeError(`alias must be at most ${MAX_ALIAS_LENGTH} characters`);
204
+ if (typeof value !== 'string')
205
+ throw new TypeError('key value is required');
206
+ const cleanValue = value.trim();
207
+ if (cleanValue.length === 0)
208
+ throw new TypeError('key value is required');
209
+ if (cleanValue.length > MAX_KEY_LENGTH)
210
+ throw new TypeError('key value is too long');
211
+ return { alias: cleanAlias, value: cleanValue };
212
+ }
213
+ /** Validate a rename/enable patch. */
214
+ export function validateKeyPatch(patch) {
215
+ if (patch === null || typeof patch !== 'object' || Array.isArray(patch))
216
+ throw new TypeError('patch is required');
217
+ const source = patch;
218
+ const result = {};
219
+ if (source.alias !== undefined) {
220
+ const alias = typeof source.alias === 'string' ? source.alias.trim() : '';
221
+ if (alias.length === 0)
222
+ throw new TypeError('alias is required');
223
+ if (alias.length > MAX_ALIAS_LENGTH)
224
+ throw new TypeError(`alias must be at most ${MAX_ALIAS_LENGTH} characters`);
225
+ result.alias = alias;
226
+ }
227
+ if (source.enabled !== undefined) {
228
+ if (typeof source.enabled !== 'boolean')
229
+ throw new TypeError('enabled must be a boolean');
230
+ result.enabled = source.enabled;
231
+ }
232
+ if (Object.keys(result).length === 0)
233
+ throw new TypeError('patch has no supported fields');
234
+ return result;
235
+ }
236
+ /** Maximum keys per provider (also used by the Host). */
237
+ export const KEY_LIMIT = MAX_KEYS_PER_PROVIDER;
238
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Shared, transport-safe domain shapes for the Keys Manager.
3
+ *
4
+ * The Host and the Web Client both compile against these declarations. They
5
+ * deliberately contain no Cordis, Node, or React types so the client bundle
6
+ * can import them type-only without pulling Host code into the browser.
7
+ */
8
+ /** Health state a key row renders. */
9
+ export type KeyStatus = 'active' | 'cooling' | 'paused' | 'missing';
10
+ /** Health state a provider row renders. */
11
+ export type ProviderStatus = KeyStatus | 'empty' | 'unmanaged';
12
+ /** Manager policy shared by the Host selection engine and the UI form. */
13
+ export interface ManagerSettings {
14
+ /** Retries on the same key before switching to another key. */
15
+ retryCount: number;
16
+ /** First cooldown after a key starts failing. */
17
+ baseCooldownMs: number;
18
+ /** A failure within this window after recovery escalates the cooldown. */
19
+ recoveryGraceMs: number;
20
+ /** Hard ceiling for any cooldown. */
21
+ maxCooldownMs: number;
22
+ /** Keep one key for one session instead of rotating every request. */
23
+ stickySession: boolean;
24
+ /** Automatically mark and switch away from a failing key. */
25
+ autoSwitch: boolean;
26
+ }
27
+ /** Reserved OAuth2 account payload; unset for API-key rows. */
28
+ export type OAuthRecord = Record<string, unknown>;
29
+ /** One persisted key. Key literals live only in the credential store. */
30
+ export interface KeyRecord {
31
+ id: string;
32
+ alias: string;
33
+ /** Credential reference holding this key's literal. */
34
+ ref: string;
35
+ kind: 'api-key' | 'oauth';
36
+ enabled: boolean;
37
+ createdAt: number;
38
+ lastUsedAt: number;
39
+ cooldownUntil: number;
40
+ lastFailureAt: number;
41
+ lastFailureCode: string;
42
+ failureLevel: number;
43
+ consecutiveFailures: number;
44
+ totalUses: number;
45
+ totalFailures: number;
46
+ valueMissing: boolean;
47
+ importedFrom: string | null;
48
+ /** Reserved for subscription OAuth2 accounts. */
49
+ oauth: OAuthRecord | null;
50
+ }
51
+ /** One model entry installed into a pi-ai route profile. */
52
+ export interface ModelRecord {
53
+ id: string;
54
+ name?: string;
55
+ contextWindow?: number;
56
+ maxTokens?: number;
57
+ input?: Array<'text' | 'image'>;
58
+ reasoningEfforts?: Record<string, unknown>;
59
+ compat?: Record<string, unknown>;
60
+ }
61
+ /** One managed provider and its adoption facts. */
62
+ export interface ProviderRecord {
63
+ provider: string;
64
+ displayName: string;
65
+ source: 'dsh' | 'extra';
66
+ settingsNs: string;
67
+ settingsPath: string[];
68
+ adapter: 'pi-ai' | 'deepseek';
69
+ /** Broker reference written into the provider profile's `apiKeyEnv`. */
70
+ brokerRef: string;
71
+ /** The profile's reference before adoption, restored on release. */
72
+ originalApiKeyEnv: string | null;
73
+ /** True when the profile itself was created by adding an extra provider. */
74
+ createdProfile: boolean;
75
+ /** True when a profile existed before adoption. */
76
+ hadProfile: boolean;
77
+ extraId: string | null;
78
+ api: string | null;
79
+ baseURL: string | null;
80
+ models: ModelRecord[];
81
+ adoptedAt: number;
82
+ /** Next round-robin position in `keys`. */
83
+ cursor: number;
84
+ keys: KeyRecord[];
85
+ }
86
+ /** Durable, secret-free manager document. */
87
+ export interface StateDocument {
88
+ version: 1;
89
+ settings: ManagerSettings;
90
+ providers: Record<string, ProviderRecord>;
91
+ sessionBindings: Record<string, {
92
+ keyId: string;
93
+ boundAt: number;
94
+ }>;
95
+ }
96
+ /** Public key row returned to the UI (no credential reference or value). */
97
+ export interface KeyView {
98
+ id: string;
99
+ alias: string;
100
+ status: KeyStatus;
101
+ enabled: boolean;
102
+ createdAt: number;
103
+ lastUsedAt: number;
104
+ cooldownUntil: number;
105
+ failureLevel: number;
106
+ consecutiveFailures: number;
107
+ totalUses: number;
108
+ totalFailures: number;
109
+ importedFrom: string | null;
110
+ valueMissing: boolean;
111
+ kind: 'api-key' | 'oauth';
112
+ }
113
+ /** Public provider row returned to the UI. */
114
+ export interface ProviderView {
115
+ provider: string;
116
+ displayName: string;
117
+ source: 'dsh' | 'extra';
118
+ /** Where the route came from: DSH catalog, this plugin's catalog, or a hand-declared profile. */
119
+ origin?: 'dsh' | 'plugin' | 'custom';
120
+ /** True when this plugin created the route profile and release removes it. */
121
+ createdProfile?: boolean;
122
+ managed: boolean;
123
+ configured: boolean;
124
+ active: boolean;
125
+ supported: boolean;
126
+ reason?: string;
127
+ settingsNs: string;
128
+ settingsPath: string[];
129
+ apiKeyEnv: string | null;
130
+ brokerRef: string | null;
131
+ bindingHealthy?: boolean;
132
+ extra: boolean;
133
+ extraId: string | null;
134
+ api: string | null;
135
+ baseURL: string | null;
136
+ env: string[];
137
+ defaultBaseURL: string | null;
138
+ nativeAuth: {
139
+ env: string[];
140
+ oauth: boolean;
141
+ } | null;
142
+ models: ModelRecord[];
143
+ keyCount: number;
144
+ healthyKeyCount: number;
145
+ status: ProviderStatus;
146
+ keys: KeyView[];
147
+ warning?: string;
148
+ }
149
+ /** Complete manager snapshot the UI renders. */
150
+ export interface StateView {
151
+ now: number;
152
+ writable: boolean;
153
+ settings: ManagerSettings;
154
+ providers: ProviderView[];
155
+ managedCount: number;
156
+ keyCount: number;
157
+ healthyKeyCount: number;
158
+ coolingKeyCount: number;
159
+ pausedKeyCount: number;
160
+ bindingsCount: number;
161
+ warnings: string[];
162
+ }
163
+ /** One curated extra built-in provider. */
164
+ export interface ExtraProvider {
165
+ id: string;
166
+ name: string;
167
+ api: string;
168
+ baseURL: string;
169
+ env: string[];
170
+ models: ModelRecord[];
171
+ }
package/lib/domain.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared, transport-safe domain shapes for the Keys Manager.
3
+ *
4
+ * The Host and the Web Client both compile against these declarations. They
5
+ * deliberately contain no Cordis, Node, or React types so the client bundle
6
+ * can import them type-only without pulling Host code into the browser.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}