@oh-my-pi/pi-coding-agent 11.8.2 → 11.9.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/CHANGELOG.md +42 -0
- package/docs/tui.md +9 -9
- package/package.json +7 -7
- package/src/capability/mcp.ts +9 -0
- package/src/cli/file-processor.ts +8 -13
- package/src/cli/oclif-help.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/commit/git/index.ts +16 -16
- package/src/config/file-lock.ts +1 -1
- package/src/config/keybindings.ts +11 -11
- package/src/config/model-registry.ts +31 -66
- package/src/config/settings.ts +88 -95
- package/src/config.ts +2 -2
- package/src/cursor.ts +4 -4
- package/src/debug/index.ts +28 -28
- package/src/discovery/builtin.ts +48 -0
- package/src/discovery/codex.ts +5 -13
- package/src/discovery/cursor.ts +2 -7
- package/src/discovery/mcp-json.ts +33 -0
- package/src/exa/mcp-client.ts +2 -2
- package/src/exa/websets.ts +2 -2
- package/src/export/html/index.ts +3 -3
- package/src/export/ttsr.ts +27 -27
- package/src/extensibility/custom-tools/loader.ts +9 -9
- package/src/extensibility/extensions/runner.ts +64 -64
- package/src/extensibility/hooks/runner.ts +46 -46
- package/src/extensibility/plugins/manager.ts +49 -49
- package/src/extensibility/slash-commands.ts +1 -0
- package/src/index.ts +0 -3
- package/src/internal-urls/router.ts +5 -5
- package/src/ipy/kernel.ts +61 -57
- package/src/lsp/client.ts +1 -1
- package/src/lsp/clients/biome-client.ts +2 -2
- package/src/lsp/clients/lsp-linter-client.ts +7 -7
- package/src/lsp/index.ts +9 -9
- package/src/mcp/config-writer.ts +194 -0
- package/src/mcp/config.ts +20 -6
- package/src/mcp/index.ts +4 -0
- package/src/mcp/loader.ts +6 -0
- package/src/mcp/manager.ts +139 -50
- package/src/mcp/oauth-discovery.ts +274 -0
- package/src/mcp/oauth-flow.ts +229 -0
- package/src/mcp/tool-bridge.ts +20 -20
- package/src/mcp/transports/http.ts +107 -66
- package/src/mcp/transports/stdio.ts +74 -59
- package/src/mcp/types.ts +15 -1
- package/src/modes/components/assistant-message.ts +25 -25
- package/src/modes/components/bash-execution.ts +51 -51
- package/src/modes/components/bordered-loader.ts +7 -7
- package/src/modes/components/branch-summary-message.ts +7 -7
- package/src/modes/components/compaction-summary-message.ts +7 -7
- package/src/modes/components/countdown-timer.ts +15 -15
- package/src/modes/components/custom-editor.ts +22 -22
- package/src/modes/components/custom-message.ts +21 -21
- package/src/modes/components/dynamic-border.ts +3 -3
- package/src/modes/components/extensions/extension-dashboard.ts +72 -72
- package/src/modes/components/extensions/extension-list.ts +99 -97
- package/src/modes/components/extensions/inspector-panel.ts +26 -26
- package/src/modes/components/footer.ts +36 -36
- package/src/modes/components/history-search.ts +52 -52
- package/src/modes/components/hook-editor.ts +20 -20
- package/src/modes/components/hook-input.ts +20 -20
- package/src/modes/components/hook-message.ts +22 -22
- package/src/modes/components/hook-selector.ts +52 -52
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/login-dialog.ts +57 -57
- package/src/modes/components/mcp-add-wizard.ts +1286 -0
- package/src/modes/components/model-selector.ts +173 -173
- package/src/modes/components/oauth-selector.ts +45 -45
- package/src/modes/components/plugin-settings.ts +52 -52
- package/src/modes/components/python-execution.ts +53 -53
- package/src/modes/components/queue-mode-selector.ts +7 -7
- package/src/modes/components/read-tool-group.ts +23 -23
- package/src/modes/components/session-selector.ts +40 -37
- package/src/modes/components/settings-selector.ts +80 -80
- package/src/modes/components/show-images-selector.ts +7 -7
- package/src/modes/components/skill-message.ts +27 -27
- package/src/modes/components/status-line-segment-editor.ts +81 -81
- package/src/modes/components/status-line.ts +73 -73
- package/src/modes/components/theme-selector.ts +11 -11
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/todo-display.ts +19 -19
- package/src/modes/components/todo-reminder.ts +9 -9
- package/src/modes/components/tool-execution.ts +212 -216
- package/src/modes/components/tree-selector.ts +144 -144
- package/src/modes/components/ttsr-notification.ts +17 -17
- package/src/modes/components/user-message-selector.ts +18 -18
- package/src/modes/components/welcome.ts +10 -10
- package/src/modes/controllers/command-controller.ts +0 -7
- package/src/modes/controllers/event-controller.ts +23 -23
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +12 -9
- package/src/modes/controllers/mcp-command-controller.ts +1223 -0
- package/src/modes/interactive-mode.ts +240 -241
- package/src/modes/rpc/rpc-client.ts +77 -77
- package/src/modes/rpc/rpc-mode.ts +5 -5
- package/src/modes/theme/theme.ts +113 -113
- package/src/modes/types.ts +1 -1
- package/src/patch/index.ts +45 -45
- package/src/prompts/tools/task.md +22 -2
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +512 -476
- package/src/session/agent-storage.ts +72 -75
- package/src/session/auth-storage.ts +186 -252
- package/src/session/history-storage.ts +36 -38
- package/src/session/session-manager.ts +300 -299
- package/src/session/session-storage.ts +65 -90
- package/src/ssh/connection-manager.ts +9 -9
- package/src/system-prompt.ts +2 -3
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +28 -40
- package/src/task/index.ts +13 -12
- package/src/task/subprocess-tool-registry.ts +5 -5
- package/src/task/worktree.ts +8 -5
- package/src/tools/ask.ts +7 -7
- package/src/tools/bash.ts +15 -10
- package/src/tools/browser.ts +130 -127
- package/src/tools/calculator.ts +46 -46
- package/src/tools/context.ts +9 -9
- package/src/tools/exit-plan-mode.ts +5 -5
- package/src/tools/fetch.ts +5 -5
- package/src/tools/find.ts +16 -16
- package/src/tools/grep.ts +12 -24
- package/src/tools/index.ts +1 -1
- package/src/tools/notebook.ts +6 -6
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/python.ts +12 -11
- package/src/tools/read.ts +17 -17
- package/src/tools/ssh.ts +9 -9
- package/src/tools/submit-result.ts +13 -13
- package/src/tools/todo-write.ts +6 -6
- package/src/tools/write.ts +10 -10
- package/src/tui/output-block.ts +6 -6
- package/src/tui/utils.ts +9 -9
- package/src/utils/event-bus.ts +13 -11
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/ignore-files.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/providers/anthropic.ts +7 -2
- package/examples/hooks/snake.ts +0 -342
- package/src/modes/components/armin.ts +0 -379
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
zaiUsageProvider,
|
|
34
34
|
} from "@oh-my-pi/pi-ai";
|
|
35
35
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
36
|
-
import { getAgentDbPath } from "../config";
|
|
37
36
|
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
38
37
|
import { AgentStorage } from "./agent-storage";
|
|
39
38
|
|
|
@@ -141,33 +140,33 @@ class AuthStorageUsageCache implements UsageCache {
|
|
|
141
140
|
* Reads from SQLite (agent.db).
|
|
142
141
|
*/
|
|
143
142
|
export class AuthStorage {
|
|
144
|
-
|
|
143
|
+
static readonly #defaultBackoffMs = 60_000; // Default backoff when no reset time available
|
|
145
144
|
|
|
146
145
|
/** Provider -> credentials cache, populated from agent.db on reload(). */
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
#data: Map<string, StoredCredential[]> = new Map();
|
|
147
|
+
#runtimeOverrides: Map<string, string> = new Map();
|
|
149
148
|
/** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
|
|
150
|
-
|
|
149
|
+
#providerRoundRobinIndex: Map<string, number> = new Map();
|
|
151
150
|
/** Tracks the last used credential per provider for a session (used for rate-limit switching). */
|
|
152
|
-
|
|
151
|
+
#sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
|
|
153
152
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
#credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
154
|
+
#usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
155
|
+
#usageCache?: UsageCache;
|
|
156
|
+
#usageFetch: typeof fetch;
|
|
157
|
+
#usageNow: () => number;
|
|
158
|
+
#usageLogger?: UsageLogger;
|
|
159
|
+
#fallbackResolver?: (provider: string) => string | undefined;
|
|
161
160
|
|
|
162
161
|
private constructor(
|
|
163
162
|
private storage: AgentStorage,
|
|
164
163
|
options: AuthStorageOptions = {},
|
|
165
164
|
) {
|
|
166
|
-
this
|
|
167
|
-
this
|
|
168
|
-
this
|
|
169
|
-
this
|
|
170
|
-
this
|
|
165
|
+
this.#usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
166
|
+
this.#usageCache = options.usageCache ?? new AuthStorageUsageCache(this.storage);
|
|
167
|
+
this.#usageFetch = options.usageFetch ?? fetch;
|
|
168
|
+
this.#usageNow = options.usageNow ?? Date.now;
|
|
169
|
+
this.#usageLogger =
|
|
171
170
|
options.usageLogger ??
|
|
172
171
|
({
|
|
173
172
|
debug: (message, meta) => logger.debug(message, meta),
|
|
@@ -184,89 +183,19 @@ export class AuthStorage {
|
|
|
184
183
|
return new AuthStorage(storage, options);
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
/**
|
|
188
|
-
* Create an in-memory AuthStorage instance from serialized data.
|
|
189
|
-
* Used by subagent workers to bypass discovery and use parent's credentials.
|
|
190
|
-
*/
|
|
191
|
-
static async fromSerialized(data: SerializedAuthStorage, options: AuthStorageOptions = {}): Promise<AuthStorage> {
|
|
192
|
-
const dbPath = data.dbPath ?? getAgentDbPath();
|
|
193
|
-
const storage = await AgentStorage.open(dbPath);
|
|
194
|
-
|
|
195
|
-
const instance = Object.create(AuthStorage.prototype) as AuthStorage;
|
|
196
|
-
instance.storage = storage;
|
|
197
|
-
instance.data = new Map();
|
|
198
|
-
instance.runtimeOverrides = new Map();
|
|
199
|
-
instance.providerRoundRobinIndex = new Map();
|
|
200
|
-
instance.sessionLastCredential = new Map();
|
|
201
|
-
instance.credentialBackoff = new Map();
|
|
202
|
-
instance.usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
203
|
-
instance.usageCache = options.usageCache ?? new AuthStorageUsageCache(instance.storage);
|
|
204
|
-
instance.usageFetch = options.usageFetch ?? fetch;
|
|
205
|
-
instance.usageNow = options.usageNow ?? Date.now;
|
|
206
|
-
instance.usageLogger =
|
|
207
|
-
options.usageLogger ??
|
|
208
|
-
({
|
|
209
|
-
debug: (message, meta) => logger.debug(message, meta),
|
|
210
|
-
warn: (message, meta) => logger.warn(message, meta),
|
|
211
|
-
} satisfies UsageLogger);
|
|
212
|
-
|
|
213
|
-
for (const [provider, creds] of Object.entries(data.credentials)) {
|
|
214
|
-
instance.data.set(
|
|
215
|
-
provider,
|
|
216
|
-
creds.map(c => ({
|
|
217
|
-
id: c.id,
|
|
218
|
-
credential:
|
|
219
|
-
c.type === "api_key"
|
|
220
|
-
? ({ type: "api_key", key: c.data.key as string } satisfies ApiKeyCredential)
|
|
221
|
-
: ({ type: "oauth", ...c.data } as OAuthCredential),
|
|
222
|
-
})),
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
if (data.runtimeOverrides) {
|
|
226
|
-
for (const [k, v] of Object.entries(data.runtimeOverrides)) {
|
|
227
|
-
instance.runtimeOverrides.set(k, v);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return instance;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Serialize AuthStorage for passing to subagent workers.
|
|
236
|
-
* Excludes runtime state (round-robin, backoff, usage cache).
|
|
237
|
-
*/
|
|
238
|
-
serialize(): SerializedAuthStorage {
|
|
239
|
-
const credentials: SerializedAuthStorage["credentials"] = {};
|
|
240
|
-
for (const [provider, creds] of this.data.entries()) {
|
|
241
|
-
credentials[provider] = creds.map(c => ({
|
|
242
|
-
id: c.id,
|
|
243
|
-
type: c.credential.type,
|
|
244
|
-
data: c.credential.type === "api_key" ? { key: c.credential.key } : { ...c.credential },
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
const runtimeOverrides: Record<string, string> = {};
|
|
248
|
-
for (const [k, v] of this.runtimeOverrides.entries()) {
|
|
249
|
-
runtimeOverrides[k] = v;
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
credentials,
|
|
253
|
-
runtimeOverrides: Object.keys(runtimeOverrides).length > 0 ? runtimeOverrides : undefined,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
186
|
/**
|
|
258
187
|
* Set a runtime API key override (not persisted to disk).
|
|
259
188
|
* Used for CLI --api-key flag.
|
|
260
189
|
*/
|
|
261
190
|
setRuntimeApiKey(provider: string, apiKey: string): void {
|
|
262
|
-
this
|
|
191
|
+
this.#runtimeOverrides.set(provider, apiKey);
|
|
263
192
|
}
|
|
264
193
|
|
|
265
194
|
/**
|
|
266
195
|
* Remove a runtime API key override.
|
|
267
196
|
*/
|
|
268
197
|
removeRuntimeApiKey(provider: string): void {
|
|
269
|
-
this
|
|
198
|
+
this.#runtimeOverrides.delete(provider);
|
|
270
199
|
}
|
|
271
200
|
|
|
272
201
|
/**
|
|
@@ -274,7 +203,7 @@ export class AuthStorage {
|
|
|
274
203
|
* Used for custom provider keys from models.json.
|
|
275
204
|
*/
|
|
276
205
|
setFallbackResolver(resolver: (provider: string) => string | undefined): void {
|
|
277
|
-
this
|
|
206
|
+
this.#fallbackResolver = resolver;
|
|
278
207
|
}
|
|
279
208
|
|
|
280
209
|
/**
|
|
@@ -292,12 +221,12 @@ export class AuthStorage {
|
|
|
292
221
|
|
|
293
222
|
const dedupedGrouped = new Map<string, StoredCredential[]>();
|
|
294
223
|
for (const [provider, entries] of grouped.entries()) {
|
|
295
|
-
const deduped = this
|
|
224
|
+
const deduped = this.#pruneDuplicateStoredCredentials(provider, entries);
|
|
296
225
|
if (deduped.length > 0) {
|
|
297
226
|
dedupedGrouped.set(provider, deduped);
|
|
298
227
|
}
|
|
299
228
|
}
|
|
300
|
-
this
|
|
229
|
+
this.#data = dedupedGrouped;
|
|
301
230
|
}
|
|
302
231
|
|
|
303
232
|
/**
|
|
@@ -305,8 +234,8 @@ export class AuthStorage {
|
|
|
305
234
|
* @param provider - Provider name (e.g., "anthropic", "openai")
|
|
306
235
|
* @returns Array of stored credentials, empty if none exist
|
|
307
236
|
*/
|
|
308
|
-
|
|
309
|
-
return this
|
|
237
|
+
#getStoredCredentials(provider: string): StoredCredential[] {
|
|
238
|
+
return this.#data.get(provider) ?? [];
|
|
310
239
|
}
|
|
311
240
|
|
|
312
241
|
/**
|
|
@@ -315,34 +244,34 @@ export class AuthStorage {
|
|
|
315
244
|
* @param provider - Provider name (e.g., "anthropic", "openai")
|
|
316
245
|
* @param credentials - Array of stored credentials to cache
|
|
317
246
|
*/
|
|
318
|
-
|
|
247
|
+
#setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
|
|
319
248
|
if (credentials.length === 0) {
|
|
320
|
-
this
|
|
249
|
+
this.#data.delete(provider);
|
|
321
250
|
} else {
|
|
322
|
-
this
|
|
251
|
+
this.#data.set(provider, credentials);
|
|
323
252
|
}
|
|
324
253
|
}
|
|
325
254
|
|
|
326
|
-
|
|
255
|
+
#getOAuthIdentifiers(credential: OAuthCredential): string[] {
|
|
327
256
|
const identifiers: string[] = [];
|
|
328
257
|
const accountId = credential.accountId?.trim();
|
|
329
258
|
if (accountId) identifiers.push(`account:${accountId}`);
|
|
330
259
|
const email = credential.email?.trim().toLowerCase();
|
|
331
260
|
if (email) identifiers.push(`email:${email}`);
|
|
332
261
|
if (identifiers.length > 0) return identifiers;
|
|
333
|
-
const tokenIdentifiers = this
|
|
262
|
+
const tokenIdentifiers = this.#getOAuthIdentifiersFromToken(credential.access) ?? [];
|
|
334
263
|
for (const identifier of tokenIdentifiers) {
|
|
335
264
|
identifiers.push(identifier);
|
|
336
265
|
}
|
|
337
266
|
if (identifiers.length > 0) return identifiers;
|
|
338
|
-
const refreshIdentifiers = this
|
|
267
|
+
const refreshIdentifiers = this.#getOAuthIdentifiersFromToken(credential.refresh) ?? [];
|
|
339
268
|
for (const identifier of refreshIdentifiers) {
|
|
340
269
|
identifiers.push(identifier);
|
|
341
270
|
}
|
|
342
271
|
return identifiers;
|
|
343
272
|
}
|
|
344
273
|
|
|
345
|
-
|
|
274
|
+
#getOAuthIdentifiersFromToken(token: string | undefined): string[] | undefined {
|
|
346
275
|
if (!token) return undefined;
|
|
347
276
|
const parts = token.split(".");
|
|
348
277
|
if (parts.length !== 3) return undefined;
|
|
@@ -374,7 +303,7 @@ export class AuthStorage {
|
|
|
374
303
|
}
|
|
375
304
|
}
|
|
376
305
|
|
|
377
|
-
|
|
306
|
+
#dedupeOAuthCredentials(credentials: AuthCredential[]): AuthCredential[] {
|
|
378
307
|
const seen = new Set<string>();
|
|
379
308
|
const deduped: AuthCredential[] = [];
|
|
380
309
|
for (let index = credentials.length - 1; index >= 0; index -= 1) {
|
|
@@ -383,7 +312,7 @@ export class AuthStorage {
|
|
|
383
312
|
deduped.push(credential);
|
|
384
313
|
continue;
|
|
385
314
|
}
|
|
386
|
-
const identifiers = this
|
|
315
|
+
const identifiers = this.#getOAuthIdentifiers(credential);
|
|
387
316
|
if (identifiers.length === 0) {
|
|
388
317
|
deduped.push(credential);
|
|
389
318
|
continue;
|
|
@@ -399,7 +328,7 @@ export class AuthStorage {
|
|
|
399
328
|
return deduped.reverse();
|
|
400
329
|
}
|
|
401
330
|
|
|
402
|
-
|
|
331
|
+
#pruneDuplicateStoredCredentials(provider: string, entries: StoredCredential[]): StoredCredential[] {
|
|
403
332
|
const seen = new Set<string>();
|
|
404
333
|
const kept: StoredCredential[] = [];
|
|
405
334
|
const removed: StoredCredential[] = [];
|
|
@@ -410,7 +339,7 @@ export class AuthStorage {
|
|
|
410
339
|
kept.push(entry);
|
|
411
340
|
continue;
|
|
412
341
|
}
|
|
413
|
-
const identifiers = this
|
|
342
|
+
const identifiers = this.#getOAuthIdentifiers(credential);
|
|
414
343
|
if (identifiers.length === 0) {
|
|
415
344
|
kept.push(entry);
|
|
416
345
|
continue;
|
|
@@ -428,18 +357,18 @@ export class AuthStorage {
|
|
|
428
357
|
for (const entry of removed) {
|
|
429
358
|
this.storage.deleteAuthCredential(entry.id);
|
|
430
359
|
}
|
|
431
|
-
this
|
|
360
|
+
this.#resetProviderAssignments(provider);
|
|
432
361
|
}
|
|
433
362
|
return kept.reverse();
|
|
434
363
|
}
|
|
435
364
|
|
|
436
365
|
/** Returns all credentials for a provider as an array */
|
|
437
|
-
|
|
438
|
-
return this
|
|
366
|
+
#getCredentialsForProvider(provider: string): AuthCredential[] {
|
|
367
|
+
return this.#getStoredCredentials(provider).map(entry => entry.credential);
|
|
439
368
|
}
|
|
440
369
|
|
|
441
370
|
/** Composite key for round-robin tracking: "anthropic:oauth" or "openai:api_key" */
|
|
442
|
-
|
|
371
|
+
#getProviderTypeKey(provider: string, type: AuthCredential["type"]): string {
|
|
443
372
|
return `${provider}:${type}`;
|
|
444
373
|
}
|
|
445
374
|
|
|
@@ -447,11 +376,11 @@ export class AuthStorage {
|
|
|
447
376
|
* Returns next index in round-robin sequence for load distribution.
|
|
448
377
|
* Increments stored counter and wraps at total.
|
|
449
378
|
*/
|
|
450
|
-
|
|
379
|
+
#getNextRoundRobinIndex(providerKey: string, total: number): number {
|
|
451
380
|
if (total <= 1) return 0;
|
|
452
|
-
const current = this
|
|
381
|
+
const current = this.#providerRoundRobinIndex.get(providerKey) ?? -1;
|
|
453
382
|
const next = (current + 1) % total;
|
|
454
|
-
this
|
|
383
|
+
this.#providerRoundRobinIndex.set(providerKey, next);
|
|
455
384
|
return next;
|
|
456
385
|
}
|
|
457
386
|
|
|
@@ -459,7 +388,7 @@ export class AuthStorage {
|
|
|
459
388
|
* FNV-1a hash for deterministic session-to-credential mapping.
|
|
460
389
|
* Ensures the same session always starts with the same credential.
|
|
461
390
|
*/
|
|
462
|
-
|
|
391
|
+
#getHashedIndex(sessionId: string, total: number): number {
|
|
463
392
|
if (total <= 1) return 0;
|
|
464
393
|
let hash = 2166136261; // FNV offset basis
|
|
465
394
|
for (let i = 0; i < sessionId.length; i++) {
|
|
@@ -475,9 +404,11 @@ export class AuthStorage {
|
|
|
475
404
|
* Without sessionId: starts from round-robin index (load balancing).
|
|
476
405
|
* Order wraps around so all credentials are tried if earlier ones are blocked.
|
|
477
406
|
*/
|
|
478
|
-
|
|
407
|
+
#getCredentialOrder(providerKey: string, sessionId: string | undefined, total: number): number[] {
|
|
479
408
|
if (total <= 1) return [0];
|
|
480
|
-
const start = sessionId
|
|
409
|
+
const start = sessionId
|
|
410
|
+
? this.#getHashedIndex(sessionId, total)
|
|
411
|
+
: this.#getNextRoundRobinIndex(providerKey, total);
|
|
481
412
|
const order: number[] = [];
|
|
482
413
|
for (let i = 0; i < total; i++) {
|
|
483
414
|
order.push((start + i) % total);
|
|
@@ -486,15 +417,15 @@ export class AuthStorage {
|
|
|
486
417
|
}
|
|
487
418
|
|
|
488
419
|
/** Checks if a credential is temporarily blocked due to usage limits. */
|
|
489
|
-
|
|
490
|
-
const backoffMap = this
|
|
420
|
+
#isCredentialBlocked(providerKey: string, credentialIndex: number): boolean {
|
|
421
|
+
const backoffMap = this.#credentialBackoff.get(providerKey);
|
|
491
422
|
if (!backoffMap) return false;
|
|
492
423
|
const blockedUntil = backoffMap.get(credentialIndex);
|
|
493
424
|
if (!blockedUntil) return false;
|
|
494
425
|
if (blockedUntil <= Date.now()) {
|
|
495
426
|
backoffMap.delete(credentialIndex);
|
|
496
427
|
if (backoffMap.size === 0) {
|
|
497
|
-
this
|
|
428
|
+
this.#credentialBackoff.delete(providerKey);
|
|
498
429
|
}
|
|
499
430
|
return false;
|
|
500
431
|
}
|
|
@@ -502,33 +433,33 @@ export class AuthStorage {
|
|
|
502
433
|
}
|
|
503
434
|
|
|
504
435
|
/** Marks a credential as blocked until the specified time. */
|
|
505
|
-
|
|
506
|
-
const backoffMap = this
|
|
436
|
+
#markCredentialBlocked(providerKey: string, credentialIndex: number, blockedUntilMs: number): void {
|
|
437
|
+
const backoffMap = this.#credentialBackoff.get(providerKey) ?? new Map<number, number>();
|
|
507
438
|
const existing = backoffMap.get(credentialIndex) ?? 0;
|
|
508
439
|
backoffMap.set(credentialIndex, Math.max(existing, blockedUntilMs));
|
|
509
|
-
this
|
|
440
|
+
this.#credentialBackoff.set(providerKey, backoffMap);
|
|
510
441
|
}
|
|
511
442
|
|
|
512
443
|
/** Records which credential was used for a session (for rate-limit switching). */
|
|
513
|
-
|
|
444
|
+
#recordSessionCredential(
|
|
514
445
|
provider: string,
|
|
515
446
|
sessionId: string | undefined,
|
|
516
447
|
type: AuthCredential["type"],
|
|
517
448
|
index: number,
|
|
518
449
|
): void {
|
|
519
450
|
if (!sessionId) return;
|
|
520
|
-
const sessionMap = this
|
|
451
|
+
const sessionMap = this.#sessionLastCredential.get(provider) ?? new Map();
|
|
521
452
|
sessionMap.set(sessionId, { type, index });
|
|
522
|
-
this
|
|
453
|
+
this.#sessionLastCredential.set(provider, sessionMap);
|
|
523
454
|
}
|
|
524
455
|
|
|
525
456
|
/** Retrieves the last credential used by a session. */
|
|
526
|
-
|
|
457
|
+
#getSessionCredential(
|
|
527
458
|
provider: string,
|
|
528
459
|
sessionId: string | undefined,
|
|
529
460
|
): { type: AuthCredential["type"]; index: number } | undefined {
|
|
530
461
|
if (!sessionId) return undefined;
|
|
531
|
-
return this
|
|
462
|
+
return this.#sessionLastCredential.get(provider)?.get(sessionId);
|
|
532
463
|
}
|
|
533
464
|
|
|
534
465
|
/**
|
|
@@ -536,12 +467,12 @@ export class AuthStorage {
|
|
|
536
467
|
* Returns both the credential and its index in the original array (for updates/removal).
|
|
537
468
|
* Uses deterministic hashing for session stickiness and skips blocked credentials when possible.
|
|
538
469
|
*/
|
|
539
|
-
|
|
470
|
+
#selectCredentialByType<T extends AuthCredential["type"]>(
|
|
540
471
|
provider: string,
|
|
541
472
|
type: T,
|
|
542
473
|
sessionId?: string,
|
|
543
474
|
): { credential: Extract<AuthCredential, { type: T }>; index: number } | undefined {
|
|
544
|
-
const credentials = this
|
|
475
|
+
const credentials = this.#getCredentialsForProvider(provider)
|
|
545
476
|
.map((credential, index) => ({ credential, index }))
|
|
546
477
|
.filter(
|
|
547
478
|
(entry): entry is { credential: Extract<AuthCredential, { type: T }>; index: number } =>
|
|
@@ -551,13 +482,13 @@ export class AuthStorage {
|
|
|
551
482
|
if (credentials.length === 0) return undefined;
|
|
552
483
|
if (credentials.length === 1) return credentials[0];
|
|
553
484
|
|
|
554
|
-
const providerKey = this
|
|
555
|
-
const order = this
|
|
485
|
+
const providerKey = this.#getProviderTypeKey(provider, type);
|
|
486
|
+
const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
556
487
|
const fallback = credentials[order[0]];
|
|
557
488
|
|
|
558
489
|
for (const idx of order) {
|
|
559
490
|
const candidate = credentials[idx];
|
|
560
|
-
if (!this
|
|
491
|
+
if (!this.#isCredentialBlocked(providerKey, candidate.index)) {
|
|
561
492
|
return candidate;
|
|
562
493
|
}
|
|
563
494
|
}
|
|
@@ -569,49 +500,49 @@ export class AuthStorage {
|
|
|
569
500
|
* Clears round-robin and session assignment state for a provider.
|
|
570
501
|
* Called when credentials are added/removed to prevent stale index references.
|
|
571
502
|
*/
|
|
572
|
-
|
|
573
|
-
for (const key of this
|
|
503
|
+
#resetProviderAssignments(provider: string): void {
|
|
504
|
+
for (const key of this.#providerRoundRobinIndex.keys()) {
|
|
574
505
|
if (key.startsWith(`${provider}:`)) {
|
|
575
|
-
this
|
|
506
|
+
this.#providerRoundRobinIndex.delete(key);
|
|
576
507
|
}
|
|
577
508
|
}
|
|
578
|
-
this
|
|
579
|
-
for (const key of this
|
|
509
|
+
this.#sessionLastCredential.delete(provider);
|
|
510
|
+
for (const key of this.#credentialBackoff.keys()) {
|
|
580
511
|
if (key.startsWith(`${provider}:`)) {
|
|
581
|
-
this
|
|
512
|
+
this.#credentialBackoff.delete(key);
|
|
582
513
|
}
|
|
583
514
|
}
|
|
584
515
|
}
|
|
585
516
|
|
|
586
517
|
/** Updates credential at index in-place (used for OAuth token refresh) */
|
|
587
|
-
|
|
588
|
-
const entries = this
|
|
518
|
+
#replaceCredentialAt(provider: string, index: number, credential: AuthCredential): void {
|
|
519
|
+
const entries = this.#getStoredCredentials(provider);
|
|
589
520
|
if (index < 0 || index >= entries.length) return;
|
|
590
521
|
const target = entries[index];
|
|
591
522
|
this.storage.updateAuthCredential(target.id, credential);
|
|
592
523
|
const updated = [...entries];
|
|
593
524
|
updated[index] = { id: target.id, credential };
|
|
594
|
-
this
|
|
525
|
+
this.#setStoredCredentials(provider, updated);
|
|
595
526
|
}
|
|
596
527
|
|
|
597
528
|
/**
|
|
598
529
|
* Removes credential at index (used when OAuth refresh fails).
|
|
599
530
|
* Cleans up provider entry if last credential removed.
|
|
600
531
|
*/
|
|
601
|
-
|
|
602
|
-
const entries = this
|
|
532
|
+
#removeCredentialAt(provider: string, index: number): void {
|
|
533
|
+
const entries = this.#getStoredCredentials(provider);
|
|
603
534
|
if (index < 0 || index >= entries.length) return;
|
|
604
535
|
this.storage.deleteAuthCredential(entries[index].id);
|
|
605
536
|
const updated = entries.filter((_value, idx) => idx !== index);
|
|
606
|
-
this
|
|
607
|
-
this
|
|
537
|
+
this.#setStoredCredentials(provider, updated);
|
|
538
|
+
this.#resetProviderAssignments(provider);
|
|
608
539
|
}
|
|
609
540
|
|
|
610
541
|
/**
|
|
611
542
|
* Get credential for a provider (first entry if multiple).
|
|
612
543
|
*/
|
|
613
544
|
get(provider: string): AuthCredential | undefined {
|
|
614
|
-
return this
|
|
545
|
+
return this.#getCredentialsForProvider(provider)[0];
|
|
615
546
|
}
|
|
616
547
|
|
|
617
548
|
/**
|
|
@@ -619,13 +550,13 @@ export class AuthStorage {
|
|
|
619
550
|
*/
|
|
620
551
|
async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
|
|
621
552
|
const normalized = Array.isArray(credential) ? credential : [credential];
|
|
622
|
-
const deduped = this
|
|
553
|
+
const deduped = this.#dedupeOAuthCredentials(normalized);
|
|
623
554
|
const stored = this.storage.replaceAuthCredentialsForProvider(provider, deduped);
|
|
624
|
-
this
|
|
555
|
+
this.#setStoredCredentials(
|
|
625
556
|
provider,
|
|
626
557
|
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
627
558
|
);
|
|
628
|
-
this
|
|
559
|
+
this.#resetProviderAssignments(provider);
|
|
629
560
|
}
|
|
630
561
|
|
|
631
562
|
/**
|
|
@@ -633,22 +564,22 @@ export class AuthStorage {
|
|
|
633
564
|
*/
|
|
634
565
|
async remove(provider: string): Promise<void> {
|
|
635
566
|
this.storage.deleteAuthCredentialsForProvider(provider);
|
|
636
|
-
this
|
|
637
|
-
this
|
|
567
|
+
this.#data.delete(provider);
|
|
568
|
+
this.#resetProviderAssignments(provider);
|
|
638
569
|
}
|
|
639
570
|
|
|
640
571
|
/**
|
|
641
572
|
* List all providers with credentials.
|
|
642
573
|
*/
|
|
643
574
|
list(): string[] {
|
|
644
|
-
return [...this
|
|
575
|
+
return [...this.#data.keys()];
|
|
645
576
|
}
|
|
646
577
|
|
|
647
578
|
/**
|
|
648
579
|
* Check if credentials exist for a provider in agent.db.
|
|
649
580
|
*/
|
|
650
581
|
has(provider: string): boolean {
|
|
651
|
-
return this
|
|
582
|
+
return this.#getCredentialsForProvider(provider).length > 0;
|
|
652
583
|
}
|
|
653
584
|
|
|
654
585
|
/**
|
|
@@ -656,10 +587,10 @@ export class AuthStorage {
|
|
|
656
587
|
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
|
657
588
|
*/
|
|
658
589
|
hasAuth(provider: string): boolean {
|
|
659
|
-
if (this
|
|
660
|
-
if (this
|
|
590
|
+
if (this.#runtimeOverrides.has(provider)) return true;
|
|
591
|
+
if (this.#getCredentialsForProvider(provider).length > 0) return true;
|
|
661
592
|
if (getEnvApiKey(provider)) return true;
|
|
662
|
-
if (this
|
|
593
|
+
if (this.#fallbackResolver?.(provider)) return true;
|
|
663
594
|
return false;
|
|
664
595
|
}
|
|
665
596
|
|
|
@@ -667,14 +598,14 @@ export class AuthStorage {
|
|
|
667
598
|
* Check if OAuth credentials are configured for a provider.
|
|
668
599
|
*/
|
|
669
600
|
hasOAuth(provider: string): boolean {
|
|
670
|
-
return this
|
|
601
|
+
return this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth");
|
|
671
602
|
}
|
|
672
603
|
|
|
673
604
|
/**
|
|
674
605
|
* Get OAuth credentials for a provider.
|
|
675
606
|
*/
|
|
676
607
|
getOAuthCredential(provider: string): OAuthCredential | undefined {
|
|
677
|
-
return this
|
|
608
|
+
return this.#getCredentialsForProvider(provider).find(
|
|
678
609
|
(credential): credential is OAuthCredential => credential.type === "oauth",
|
|
679
610
|
);
|
|
680
611
|
}
|
|
@@ -684,7 +615,7 @@ export class AuthStorage {
|
|
|
684
615
|
*/
|
|
685
616
|
getAll(): AuthStorageData {
|
|
686
617
|
const result: AuthStorageData = {};
|
|
687
|
-
for (const [provider, entries] of this
|
|
618
|
+
for (const [provider, entries] of this.#data.entries()) {
|
|
688
619
|
const credentials = entries.map(entry => entry.credential);
|
|
689
620
|
if (credentials.length === 1) {
|
|
690
621
|
result[provider] = credentials[0];
|
|
@@ -753,7 +684,7 @@ export class AuthStorage {
|
|
|
753
684
|
}
|
|
754
685
|
|
|
755
686
|
const newCredential: OAuthCredential = { type: "oauth", ...credentials };
|
|
756
|
-
const existing = this
|
|
687
|
+
const existing = this.#getCredentialsForProvider(provider);
|
|
757
688
|
if (existing.length === 0) {
|
|
758
689
|
await this.set(provider, newCredential);
|
|
759
690
|
return;
|
|
@@ -774,7 +705,7 @@ export class AuthStorage {
|
|
|
774
705
|
// Queries provider usage endpoints to detect rate limits before they occur.
|
|
775
706
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
776
707
|
|
|
777
|
-
|
|
708
|
+
#buildUsageCredential(credential: OAuthCredential): UsageCredential {
|
|
778
709
|
return {
|
|
779
710
|
type: "oauth",
|
|
780
711
|
accessToken: credential.access,
|
|
@@ -787,14 +718,14 @@ export class AuthStorage {
|
|
|
787
718
|
};
|
|
788
719
|
}
|
|
789
720
|
|
|
790
|
-
|
|
721
|
+
#getUsageReportMetadataValue(report: UsageReport, key: string): string | undefined {
|
|
791
722
|
const metadata = report.metadata;
|
|
792
723
|
if (!metadata || typeof metadata !== "object") return undefined;
|
|
793
724
|
const value = metadata[key];
|
|
794
725
|
return typeof value === "string" ? value.trim() : undefined;
|
|
795
726
|
}
|
|
796
727
|
|
|
797
|
-
|
|
728
|
+
#getUsageReportScopeAccountId(report: UsageReport): string | undefined {
|
|
798
729
|
const ids = new Set<string>();
|
|
799
730
|
for (const limit of report.limits) {
|
|
800
731
|
const accountId = limit.scope.accountId?.trim();
|
|
@@ -804,24 +735,24 @@ export class AuthStorage {
|
|
|
804
735
|
return undefined;
|
|
805
736
|
}
|
|
806
737
|
|
|
807
|
-
|
|
738
|
+
#getUsageReportIdentifiers(report: UsageReport): string[] {
|
|
808
739
|
const identifiers: string[] = [];
|
|
809
|
-
const email = this
|
|
740
|
+
const email = this.#getUsageReportMetadataValue(report, "email");
|
|
810
741
|
if (email) identifiers.push(`email:${email.toLowerCase()}`);
|
|
811
|
-
const accountId = this
|
|
742
|
+
const accountId = this.#getUsageReportMetadataValue(report, "accountId");
|
|
812
743
|
if (accountId) identifiers.push(`account:${accountId}`);
|
|
813
|
-
const account = this
|
|
744
|
+
const account = this.#getUsageReportMetadataValue(report, "account");
|
|
814
745
|
if (account) identifiers.push(`account:${account}`);
|
|
815
|
-
const user = this
|
|
746
|
+
const user = this.#getUsageReportMetadataValue(report, "user");
|
|
816
747
|
if (user) identifiers.push(`account:${user}`);
|
|
817
|
-
const username = this
|
|
748
|
+
const username = this.#getUsageReportMetadataValue(report, "username");
|
|
818
749
|
if (username) identifiers.push(`account:${username}`);
|
|
819
|
-
const scopeAccountId = this
|
|
750
|
+
const scopeAccountId = this.#getUsageReportScopeAccountId(report);
|
|
820
751
|
if (scopeAccountId) identifiers.push(`account:${scopeAccountId}`);
|
|
821
752
|
return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
|
|
822
753
|
}
|
|
823
754
|
|
|
824
|
-
|
|
755
|
+
#mergeUsageReportGroup(reports: UsageReport[]): UsageReport {
|
|
825
756
|
if (reports.length === 1) return reports[0];
|
|
826
757
|
const sorted = [...reports].sort((a, b) => {
|
|
827
758
|
const limitDiff = b.limits.length - a.limits.length;
|
|
@@ -859,12 +790,12 @@ export class AuthStorage {
|
|
|
859
790
|
};
|
|
860
791
|
}
|
|
861
792
|
|
|
862
|
-
|
|
793
|
+
#dedupeUsageReports(reports: UsageReport[]): UsageReport[] {
|
|
863
794
|
const groups: UsageReport[][] = [];
|
|
864
795
|
const idToGroup = new Map<string, number>();
|
|
865
796
|
|
|
866
797
|
for (const report of reports) {
|
|
867
|
-
const identifiers = this
|
|
798
|
+
const identifiers = this.#getUsageReportIdentifiers(report);
|
|
868
799
|
let groupIndex: number | undefined;
|
|
869
800
|
for (const identifier of identifiers) {
|
|
870
801
|
const existing = idToGroup.get(identifier);
|
|
@@ -883,9 +814,9 @@ export class AuthStorage {
|
|
|
883
814
|
}
|
|
884
815
|
}
|
|
885
816
|
|
|
886
|
-
const deduped = groups.map(group => this
|
|
817
|
+
const deduped = groups.map(group => this.#mergeUsageReportGroup(group));
|
|
887
818
|
if (deduped.length !== reports.length) {
|
|
888
|
-
this
|
|
819
|
+
this.#usageLogger?.debug("Usage reports deduped", {
|
|
889
820
|
before: reports.length,
|
|
890
821
|
after: deduped.length,
|
|
891
822
|
});
|
|
@@ -893,7 +824,7 @@ export class AuthStorage {
|
|
|
893
824
|
return deduped;
|
|
894
825
|
}
|
|
895
826
|
|
|
896
|
-
|
|
827
|
+
#isUsageLimitExhausted(limit: UsageLimit): boolean {
|
|
897
828
|
if (limit.status === "exhausted") return true;
|
|
898
829
|
const amount = limit.amount;
|
|
899
830
|
if (amount.usedFraction !== undefined && amount.usedFraction >= 1) return true;
|
|
@@ -905,15 +836,15 @@ export class AuthStorage {
|
|
|
905
836
|
}
|
|
906
837
|
|
|
907
838
|
/** Returns true if usage indicates rate limit has been reached. */
|
|
908
|
-
|
|
909
|
-
return report.limits.some(limit => this
|
|
839
|
+
#isUsageLimitReached(report: UsageReport): boolean {
|
|
840
|
+
return report.limits.some(limit => this.#isUsageLimitExhausted(limit));
|
|
910
841
|
}
|
|
911
842
|
|
|
912
843
|
/** Extracts the earliest reset timestamp from exhausted windows (in ms). */
|
|
913
|
-
|
|
844
|
+
#getUsageResetAtMs(report: UsageReport, nowMs: number): number | undefined {
|
|
914
845
|
const candidates: number[] = [];
|
|
915
846
|
for (const limit of report.limits) {
|
|
916
|
-
if (!this
|
|
847
|
+
if (!this.#isUsageLimitExhausted(limit)) continue;
|
|
917
848
|
const window = limit.window;
|
|
918
849
|
if (window?.resetsAt && window.resetsAt > nowMs) {
|
|
919
850
|
candidates.push(window.resetsAt);
|
|
@@ -927,13 +858,13 @@ export class AuthStorage {
|
|
|
927
858
|
return Math.min(...candidates);
|
|
928
859
|
}
|
|
929
860
|
|
|
930
|
-
|
|
861
|
+
async #getUsageReport(
|
|
931
862
|
provider: Provider,
|
|
932
863
|
credential: OAuthCredential,
|
|
933
864
|
options?: { baseUrl?: string },
|
|
934
865
|
): Promise<UsageReport | null> {
|
|
935
|
-
const resolver = this
|
|
936
|
-
const cache = this
|
|
866
|
+
const resolver = this.#usageProviderResolver;
|
|
867
|
+
const cache = this.#usageCache;
|
|
937
868
|
if (!resolver || !cache) return null;
|
|
938
869
|
|
|
939
870
|
const providerImpl = resolver(provider);
|
|
@@ -941,7 +872,7 @@ export class AuthStorage {
|
|
|
941
872
|
|
|
942
873
|
const params = {
|
|
943
874
|
provider,
|
|
944
|
-
credential: this
|
|
875
|
+
credential: this.#buildUsageCredential(credential),
|
|
945
876
|
baseUrl: options?.baseUrl,
|
|
946
877
|
};
|
|
947
878
|
|
|
@@ -950,9 +881,9 @@ export class AuthStorage {
|
|
|
950
881
|
try {
|
|
951
882
|
return await providerImpl.fetchUsage(params, {
|
|
952
883
|
cache,
|
|
953
|
-
fetch: this
|
|
954
|
-
now: this
|
|
955
|
-
logger: this
|
|
884
|
+
fetch: this.#usageFetch,
|
|
885
|
+
now: this.#usageNow,
|
|
886
|
+
logger: this.#usageLogger,
|
|
956
887
|
});
|
|
957
888
|
} catch (error) {
|
|
958
889
|
logger.debug("AuthStorage usage fetch failed", {
|
|
@@ -966,30 +897,33 @@ export class AuthStorage {
|
|
|
966
897
|
async fetchUsageReports(options?: {
|
|
967
898
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
968
899
|
}): Promise<UsageReport[] | null> {
|
|
969
|
-
const resolver = this
|
|
970
|
-
const cache = this
|
|
900
|
+
const resolver = this.#usageProviderResolver;
|
|
901
|
+
const cache = this.#usageCache;
|
|
971
902
|
if (!resolver || !cache) return null;
|
|
972
903
|
|
|
973
904
|
const tasks: Array<Promise<UsageReport | null>> = [];
|
|
974
|
-
const providers = new Set<string>([
|
|
975
|
-
|
|
905
|
+
const providers = new Set<string>([
|
|
906
|
+
...this.#data.keys(),
|
|
907
|
+
...DEFAULT_USAGE_PROVIDERS.map(provider => provider.id),
|
|
908
|
+
]);
|
|
909
|
+
this.#usageLogger?.debug("Usage fetch requested", {
|
|
976
910
|
providers: Array.from(providers).sort(),
|
|
977
911
|
});
|
|
978
912
|
for (const provider of providers) {
|
|
979
913
|
const providerImpl = resolver(provider as Provider);
|
|
980
914
|
if (!providerImpl) continue;
|
|
981
915
|
const baseUrl = options?.baseUrlResolver?.(provider as Provider);
|
|
982
|
-
let entries = this
|
|
916
|
+
let entries = this.#getStoredCredentials(provider);
|
|
983
917
|
if (entries.length > 0) {
|
|
984
|
-
const dedupedEntries = this
|
|
918
|
+
const dedupedEntries = this.#pruneDuplicateStoredCredentials(provider, entries);
|
|
985
919
|
if (dedupedEntries.length !== entries.length) {
|
|
986
|
-
this
|
|
920
|
+
this.#setStoredCredentials(provider, dedupedEntries);
|
|
987
921
|
}
|
|
988
922
|
entries = dedupedEntries;
|
|
989
923
|
}
|
|
990
924
|
|
|
991
925
|
if (entries.length === 0) {
|
|
992
|
-
const runtimeKey = this
|
|
926
|
+
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
993
927
|
const envKey = getEnvApiKey(provider);
|
|
994
928
|
const apiKey = runtimeKey ?? envKey;
|
|
995
929
|
if (!apiKey) {
|
|
@@ -1003,7 +937,7 @@ export class AuthStorage {
|
|
|
1003
937
|
if (providerImpl.supports && !providerImpl.supports(params)) {
|
|
1004
938
|
continue;
|
|
1005
939
|
}
|
|
1006
|
-
this
|
|
940
|
+
this.#usageLogger?.debug("Usage fetch queued", {
|
|
1007
941
|
provider,
|
|
1008
942
|
credentialType: "api_key",
|
|
1009
943
|
baseUrl,
|
|
@@ -1012,9 +946,9 @@ export class AuthStorage {
|
|
|
1012
946
|
providerImpl
|
|
1013
947
|
.fetchUsage(params, {
|
|
1014
948
|
cache,
|
|
1015
|
-
fetch: this
|
|
1016
|
-
now: this
|
|
1017
|
-
logger: this
|
|
949
|
+
fetch: this.#usageFetch,
|
|
950
|
+
now: this.#usageNow,
|
|
951
|
+
logger: this.#usageLogger,
|
|
1018
952
|
})
|
|
1019
953
|
.catch(error => {
|
|
1020
954
|
logger.debug("AuthStorage usage fetch failed", {
|
|
@@ -1032,7 +966,7 @@ export class AuthStorage {
|
|
|
1032
966
|
const usageCredential: UsageCredential =
|
|
1033
967
|
credential.type === "api_key"
|
|
1034
968
|
? { type: "api_key", apiKey: credential.key }
|
|
1035
|
-
: this
|
|
969
|
+
: this.#buildUsageCredential(credential);
|
|
1036
970
|
const params = {
|
|
1037
971
|
provider: provider as Provider,
|
|
1038
972
|
credential: usageCredential,
|
|
@@ -1043,7 +977,7 @@ export class AuthStorage {
|
|
|
1043
977
|
continue;
|
|
1044
978
|
}
|
|
1045
979
|
|
|
1046
|
-
this
|
|
980
|
+
this.#usageLogger?.debug("Usage fetch queued", {
|
|
1047
981
|
provider,
|
|
1048
982
|
credentialType: usageCredential.type,
|
|
1049
983
|
baseUrl,
|
|
@@ -1055,9 +989,9 @@ export class AuthStorage {
|
|
|
1055
989
|
providerImpl
|
|
1056
990
|
.fetchUsage(params, {
|
|
1057
991
|
cache,
|
|
1058
|
-
fetch: this
|
|
1059
|
-
now: this
|
|
1060
|
-
logger: this
|
|
992
|
+
fetch: this.#usageFetch,
|
|
993
|
+
now: this.#usageNow,
|
|
994
|
+
logger: this.#usageLogger,
|
|
1061
995
|
})
|
|
1062
996
|
.catch(error => {
|
|
1063
997
|
logger.debug("AuthStorage usage fetch failed", {
|
|
@@ -1073,16 +1007,16 @@ export class AuthStorage {
|
|
|
1073
1007
|
if (tasks.length === 0) return [];
|
|
1074
1008
|
const results = await Promise.all(tasks);
|
|
1075
1009
|
const reports = results.filter((report): report is UsageReport => report !== null);
|
|
1076
|
-
const deduped = this
|
|
1077
|
-
this
|
|
1010
|
+
const deduped = this.#dedupeUsageReports(reports);
|
|
1011
|
+
this.#usageLogger?.debug("Usage fetch resolved", {
|
|
1078
1012
|
reports: deduped.map(report => {
|
|
1079
1013
|
const accountLabel =
|
|
1080
|
-
this
|
|
1081
|
-
this
|
|
1082
|
-
this
|
|
1083
|
-
this
|
|
1084
|
-
this
|
|
1085
|
-
this
|
|
1014
|
+
this.#getUsageReportMetadataValue(report, "email") ??
|
|
1015
|
+
this.#getUsageReportMetadataValue(report, "accountId") ??
|
|
1016
|
+
this.#getUsageReportMetadataValue(report, "account") ??
|
|
1017
|
+
this.#getUsageReportMetadataValue(report, "user") ??
|
|
1018
|
+
this.#getUsageReportMetadataValue(report, "username") ??
|
|
1019
|
+
this.#getUsageReportScopeAccountId(report);
|
|
1086
1020
|
return {
|
|
1087
1021
|
provider: report.provider,
|
|
1088
1022
|
limits: report.limits.length,
|
|
@@ -1103,19 +1037,19 @@ export class AuthStorage {
|
|
|
1103
1037
|
sessionId: string | undefined,
|
|
1104
1038
|
options?: { retryAfterMs?: number; baseUrl?: string },
|
|
1105
1039
|
): Promise<boolean> {
|
|
1106
|
-
const sessionCredential = this
|
|
1040
|
+
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
1107
1041
|
if (!sessionCredential) return false;
|
|
1108
1042
|
|
|
1109
|
-
const providerKey = this
|
|
1110
|
-
const now = this
|
|
1111
|
-
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage
|
|
1043
|
+
const providerKey = this.#getProviderTypeKey(provider, sessionCredential.type);
|
|
1044
|
+
const now = this.#usageNow();
|
|
1045
|
+
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
1112
1046
|
|
|
1113
1047
|
if (provider === "openai-codex" && sessionCredential.type === "oauth") {
|
|
1114
|
-
const credential = this
|
|
1048
|
+
const credential = this.#getCredentialsForProvider(provider)[sessionCredential.index];
|
|
1115
1049
|
if (credential?.type === "oauth") {
|
|
1116
|
-
const report = await this
|
|
1117
|
-
if (report && this
|
|
1118
|
-
const resetAtMs = this
|
|
1050
|
+
const report = await this.#getUsageReport(provider, credential, options);
|
|
1051
|
+
if (report && this.#isUsageLimitReached(report)) {
|
|
1052
|
+
const resetAtMs = this.#getUsageResetAtMs(report, this.#usageNow());
|
|
1119
1053
|
if (resetAtMs && resetAtMs > blockedUntil) {
|
|
1120
1054
|
blockedUntil = resetAtMs;
|
|
1121
1055
|
}
|
|
@@ -1123,16 +1057,16 @@ export class AuthStorage {
|
|
|
1123
1057
|
}
|
|
1124
1058
|
}
|
|
1125
1059
|
|
|
1126
|
-
this
|
|
1060
|
+
this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil);
|
|
1127
1061
|
|
|
1128
|
-
const remainingCredentials = this
|
|
1062
|
+
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
1129
1063
|
.map((credential, index) => ({ credential, index }))
|
|
1130
1064
|
.filter(
|
|
1131
1065
|
(entry): entry is { credential: AuthCredential; index: number } =>
|
|
1132
1066
|
entry.credential.type === sessionCredential.type && entry.index !== sessionCredential.index,
|
|
1133
1067
|
);
|
|
1134
1068
|
|
|
1135
|
-
return remainingCredentials.some(candidate => !this
|
|
1069
|
+
return remainingCredentials.some(candidate => !this.#isCredentialBlocked(providerKey, candidate.index));
|
|
1136
1070
|
}
|
|
1137
1071
|
|
|
1138
1072
|
/**
|
|
@@ -1140,25 +1074,25 @@ export class AuthStorage {
|
|
|
1140
1074
|
* Skips blocked credentials and checks usage limits for providers with usage data.
|
|
1141
1075
|
* Falls back to earliest-unblocking credential if all are blocked.
|
|
1142
1076
|
*/
|
|
1143
|
-
|
|
1077
|
+
async #resolveOAuthApiKey(
|
|
1144
1078
|
provider: string,
|
|
1145
1079
|
sessionId?: string,
|
|
1146
1080
|
options?: { baseUrl?: string },
|
|
1147
1081
|
): Promise<string | undefined> {
|
|
1148
|
-
const credentials = this
|
|
1082
|
+
const credentials = this.#getCredentialsForProvider(provider)
|
|
1149
1083
|
.map((credential, index) => ({ credential, index }))
|
|
1150
1084
|
.filter((entry): entry is { credential: OAuthCredential; index: number } => entry.credential.type === "oauth");
|
|
1151
1085
|
|
|
1152
1086
|
if (credentials.length === 0) return undefined;
|
|
1153
1087
|
|
|
1154
|
-
const providerKey = this
|
|
1155
|
-
const order = this
|
|
1088
|
+
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
1089
|
+
const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
1156
1090
|
const fallback = credentials[order[0]];
|
|
1157
1091
|
const checkUsage = provider === "openai-codex" && credentials.length > 1;
|
|
1158
1092
|
|
|
1159
1093
|
for (const idx of order) {
|
|
1160
1094
|
const selection = credentials[idx];
|
|
1161
|
-
const apiKey = await this
|
|
1095
|
+
const apiKey = await this.#tryOAuthCredential(
|
|
1162
1096
|
provider,
|
|
1163
1097
|
selection,
|
|
1164
1098
|
providerKey,
|
|
@@ -1170,15 +1104,15 @@ export class AuthStorage {
|
|
|
1170
1104
|
if (apiKey) return apiKey;
|
|
1171
1105
|
}
|
|
1172
1106
|
|
|
1173
|
-
if (fallback && this
|
|
1174
|
-
return this
|
|
1107
|
+
if (fallback && this.#isCredentialBlocked(providerKey, fallback.index)) {
|
|
1108
|
+
return this.#tryOAuthCredential(provider, fallback, providerKey, sessionId, options, checkUsage, true);
|
|
1175
1109
|
}
|
|
1176
1110
|
|
|
1177
1111
|
return undefined;
|
|
1178
1112
|
}
|
|
1179
1113
|
|
|
1180
1114
|
/** Attempts to use a single OAuth credential, checking usage and refreshing token. */
|
|
1181
|
-
|
|
1115
|
+
async #tryOAuthCredential(
|
|
1182
1116
|
provider: string,
|
|
1183
1117
|
selection: { credential: OAuthCredential; index: number },
|
|
1184
1118
|
providerKey: string,
|
|
@@ -1187,7 +1121,7 @@ export class AuthStorage {
|
|
|
1187
1121
|
checkUsage: boolean,
|
|
1188
1122
|
allowBlocked: boolean,
|
|
1189
1123
|
): Promise<string | undefined> {
|
|
1190
|
-
if (!allowBlocked && this
|
|
1124
|
+
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
1191
1125
|
return undefined;
|
|
1192
1126
|
}
|
|
1193
1127
|
|
|
@@ -1195,14 +1129,14 @@ export class AuthStorage {
|
|
|
1195
1129
|
let usageChecked = false;
|
|
1196
1130
|
|
|
1197
1131
|
if (checkUsage) {
|
|
1198
|
-
usage = await this
|
|
1132
|
+
usage = await this.#getUsageReport(provider, selection.credential, options);
|
|
1199
1133
|
usageChecked = true;
|
|
1200
|
-
if (usage && this
|
|
1201
|
-
const resetAtMs = this
|
|
1202
|
-
this
|
|
1134
|
+
if (usage && this.#isUsageLimitReached(usage)) {
|
|
1135
|
+
const resetAtMs = this.#getUsageResetAtMs(usage, this.#usageNow());
|
|
1136
|
+
this.#markCredentialBlocked(
|
|
1203
1137
|
providerKey,
|
|
1204
1138
|
selection.index,
|
|
1205
|
-
resetAtMs ?? this
|
|
1139
|
+
resetAtMs ?? this.#usageNow() + AuthStorage.#defaultBackoffMs,
|
|
1206
1140
|
);
|
|
1207
1141
|
return undefined;
|
|
1208
1142
|
}
|
|
@@ -1226,25 +1160,25 @@ export class AuthStorage {
|
|
|
1226
1160
|
projectId: result.newCredentials.projectId ?? selection.credential.projectId,
|
|
1227
1161
|
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
1228
1162
|
};
|
|
1229
|
-
this
|
|
1163
|
+
this.#replaceCredentialAt(provider, selection.index, updated);
|
|
1230
1164
|
|
|
1231
1165
|
if (checkUsage) {
|
|
1232
1166
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
1233
1167
|
if (!usageChecked || !sameAccount) {
|
|
1234
|
-
usage = await this
|
|
1168
|
+
usage = await this.#getUsageReport(provider, updated, options);
|
|
1235
1169
|
}
|
|
1236
|
-
if (usage && this
|
|
1237
|
-
const resetAtMs = this
|
|
1238
|
-
this
|
|
1170
|
+
if (usage && this.#isUsageLimitReached(usage)) {
|
|
1171
|
+
const resetAtMs = this.#getUsageResetAtMs(usage, this.#usageNow());
|
|
1172
|
+
this.#markCredentialBlocked(
|
|
1239
1173
|
providerKey,
|
|
1240
1174
|
selection.index,
|
|
1241
|
-
resetAtMs ?? this
|
|
1175
|
+
resetAtMs ?? this.#usageNow() + AuthStorage.#defaultBackoffMs,
|
|
1242
1176
|
);
|
|
1243
1177
|
return undefined;
|
|
1244
1178
|
}
|
|
1245
1179
|
}
|
|
1246
1180
|
|
|
1247
|
-
this
|
|
1181
|
+
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
1248
1182
|
return result.apiKey;
|
|
1249
1183
|
} catch (error) {
|
|
1250
1184
|
const errorMsg = String(error);
|
|
@@ -1263,13 +1197,13 @@ export class AuthStorage {
|
|
|
1263
1197
|
|
|
1264
1198
|
if (isDefinitiveFailure) {
|
|
1265
1199
|
// Permanently remove invalid credentials
|
|
1266
|
-
this
|
|
1267
|
-
if (this
|
|
1200
|
+
this.#removeCredentialAt(provider, selection.index);
|
|
1201
|
+
if (this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth")) {
|
|
1268
1202
|
return this.getApiKey(provider, sessionId, options);
|
|
1269
1203
|
}
|
|
1270
1204
|
} else {
|
|
1271
1205
|
// Block temporarily for transient failures (5 minutes)
|
|
1272
|
-
this
|
|
1206
|
+
this.#markCredentialBlocked(providerKey, selection.index, this.#usageNow() + 5 * 60 * 1000);
|
|
1273
1207
|
}
|
|
1274
1208
|
}
|
|
1275
1209
|
|
|
@@ -1287,18 +1221,18 @@ export class AuthStorage {
|
|
|
1287
1221
|
*/
|
|
1288
1222
|
async getApiKey(provider: string, sessionId?: string, options?: { baseUrl?: string }): Promise<string | undefined> {
|
|
1289
1223
|
// Runtime override takes highest priority
|
|
1290
|
-
const runtimeKey = this
|
|
1224
|
+
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
1291
1225
|
if (runtimeKey) {
|
|
1292
1226
|
return runtimeKey;
|
|
1293
1227
|
}
|
|
1294
1228
|
|
|
1295
|
-
const apiKeySelection = this
|
|
1229
|
+
const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId);
|
|
1296
1230
|
if (apiKeySelection) {
|
|
1297
|
-
this
|
|
1231
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
1298
1232
|
return resolveConfigValue(apiKeySelection.credential.key);
|
|
1299
1233
|
}
|
|
1300
1234
|
|
|
1301
|
-
const oauthKey = await this
|
|
1235
|
+
const oauthKey = await this.#resolveOAuthApiKey(provider, sessionId, options);
|
|
1302
1236
|
if (oauthKey) {
|
|
1303
1237
|
return oauthKey;
|
|
1304
1238
|
}
|
|
@@ -1308,6 +1242,6 @@ export class AuthStorage {
|
|
|
1308
1242
|
if (envKey) return envKey;
|
|
1309
1243
|
|
|
1310
1244
|
// Fall back to custom resolver (e.g., models.json custom providers)
|
|
1311
|
-
return this
|
|
1245
|
+
return this.#fallbackResolver?.(provider) ?? undefined;
|
|
1312
1246
|
}
|
|
1313
1247
|
}
|