gsd-pi 2.10.2 → 2.10.5
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/README.md +2 -0
- package/dist/cli.js +7 -0
- package/dist/loader.js +1 -0
- package/dist/onboarding.js +104 -59
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.js +40 -0
- package/node_modules/@gsd/native/dist/hasher/index.d.ts +32 -0
- package/node_modules/@gsd/native/dist/hasher/index.js +37 -0
- package/node_modules/@gsd/native/dist/native.d.ts +4 -1
- package/node_modules/@gsd/native/dist/native.js +39 -9
- package/node_modules/@gsd/native/dist/xxhash/index.d.ts +14 -0
- package/node_modules/@gsd/native/dist/xxhash/index.js +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/node_modules/@gsd/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/package.json +8 -2
- package/packages/native/dist/hasher/index.d.ts +32 -0
- package/packages/native/dist/hasher/index.js +37 -0
- package/packages/native/dist/native.d.ts +4 -1
- package/packages/native/dist/native.js +39 -9
- package/packages/native/dist/xxhash/index.d.ts +14 -0
- package/packages/native/dist/xxhash/index.js +17 -0
- package/packages/native/src/native.ts +39 -9
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -2
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/packages/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/packages/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/packages/pi-coding-agent/src/index.ts +6 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
- package/src/resources/extensions/async-jobs/await-tool.ts +101 -0
- package/src/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
- package/src/resources/extensions/async-jobs/index.ts +133 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +250 -0
- package/src/resources/extensions/gsd/git-service.ts +13 -3
- package/src/resources/extensions/gsd/prompts/system.md +5 -2
- package/src/resources/extensions/gsd/tests/git-service.test.ts +36 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Credential storage for API keys and OAuth tokens.
|
|
3
3
|
* Handles loading, saving, and refreshing credentials from auth.json.
|
|
4
4
|
*
|
|
5
|
+
* Supports multiple credentials per provider with round-robin selection,
|
|
6
|
+
* session-sticky hashing, and automatic rate-limit fallback.
|
|
7
|
+
*
|
|
5
8
|
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
9
|
* try to refresh tokens simultaneously.
|
|
7
10
|
*/
|
|
@@ -30,7 +33,11 @@ export type OAuthCredential = {
|
|
|
30
33
|
|
|
31
34
|
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
/**
|
|
37
|
+
* On-disk format: each provider maps to a single credential or an array of credentials.
|
|
38
|
+
* Single credentials are normalized to arrays at load time for internal use.
|
|
39
|
+
*/
|
|
40
|
+
export type AuthStorageData = Record<string, AuthCredential | AuthCredential[]>;
|
|
34
41
|
|
|
35
42
|
type LockResult<T> = {
|
|
36
43
|
result: T;
|
|
@@ -178,8 +185,49 @@ export class InMemoryAuthStorageBackend implements AuthStorageBackend {
|
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Backoff durations for different error types (milliseconds)
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
const BACKOFF_RATE_LIMIT_MS = 30_000; // 30s for rate limit / 429
|
|
193
|
+
const BACKOFF_QUOTA_EXHAUSTED_MS = 30 * 60_000; // 30min for quota exhausted
|
|
194
|
+
const BACKOFF_SERVER_ERROR_MS = 20_000; // 20s for 5xx server errors
|
|
195
|
+
const BACKOFF_DEFAULT_MS = 60_000; // 60s fallback
|
|
196
|
+
|
|
197
|
+
export type UsageLimitErrorType = "rate_limit" | "quota_exhausted" | "server_error" | "unknown";
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get backoff duration for an error type.
|
|
201
|
+
*/
|
|
202
|
+
function getBackoffDuration(errorType: UsageLimitErrorType): number {
|
|
203
|
+
switch (errorType) {
|
|
204
|
+
case "rate_limit":
|
|
205
|
+
return BACKOFF_RATE_LIMIT_MS;
|
|
206
|
+
case "quota_exhausted":
|
|
207
|
+
return BACKOFF_QUOTA_EXHAUSTED_MS;
|
|
208
|
+
case "server_error":
|
|
209
|
+
return BACKOFF_SERVER_ERROR_MS;
|
|
210
|
+
default:
|
|
211
|
+
return BACKOFF_DEFAULT_MS;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Simple string hash for session-sticky credential selection.
|
|
217
|
+
* Returns a positive integer.
|
|
218
|
+
*/
|
|
219
|
+
function hashString(str: string): number {
|
|
220
|
+
let hash = 0;
|
|
221
|
+
for (let i = 0; i < str.length; i++) {
|
|
222
|
+
const char = str.charCodeAt(i);
|
|
223
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
224
|
+
}
|
|
225
|
+
return Math.abs(hash);
|
|
226
|
+
}
|
|
227
|
+
|
|
181
228
|
/**
|
|
182
229
|
* Credential storage backed by a JSON file.
|
|
230
|
+
* Supports multiple credentials per provider with round-robin rotation and rate-limit fallback.
|
|
183
231
|
*/
|
|
184
232
|
export class AuthStorage {
|
|
185
233
|
private data: AuthStorageData = {};
|
|
@@ -188,6 +236,18 @@ export class AuthStorage {
|
|
|
188
236
|
private loadError: Error | null = null;
|
|
189
237
|
private errors: Error[] = [];
|
|
190
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Round-robin index per provider. Incremented on each call to getApiKey
|
|
241
|
+
* when no sessionId is provided.
|
|
242
|
+
*/
|
|
243
|
+
private providerRoundRobinIndex: Map<string, number> = new Map();
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Backoff tracking per provider per credential index.
|
|
247
|
+
* Map<provider, Map<credentialIndex, backoffExpiresAt>>
|
|
248
|
+
*/
|
|
249
|
+
private credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
250
|
+
|
|
191
251
|
private constructor(private storage: AuthStorageBackend) {
|
|
192
252
|
this.reload();
|
|
193
253
|
}
|
|
@@ -241,6 +301,17 @@ export class AuthStorage {
|
|
|
241
301
|
return JSON.parse(content) as AuthStorageData;
|
|
242
302
|
}
|
|
243
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Normalize a storage entry to an array of credentials.
|
|
306
|
+
* Handles both single credential (backward compat) and array formats.
|
|
307
|
+
*/
|
|
308
|
+
getCredentialsForProvider(provider: string): AuthCredential[] {
|
|
309
|
+
const entry = this.data[provider];
|
|
310
|
+
if (!entry) return [];
|
|
311
|
+
if (Array.isArray(entry)) return entry;
|
|
312
|
+
return [entry];
|
|
313
|
+
}
|
|
314
|
+
|
|
244
315
|
/**
|
|
245
316
|
* Reload credentials from storage.
|
|
246
317
|
*/
|
|
@@ -259,7 +330,7 @@ export class AuthStorage {
|
|
|
259
330
|
}
|
|
260
331
|
}
|
|
261
332
|
|
|
262
|
-
private persistProviderChange(provider: string, credential: AuthCredential | undefined): void {
|
|
333
|
+
private persistProviderChange(provider: string, credential: AuthCredential | AuthCredential[] | undefined): void {
|
|
263
334
|
if (this.loadError) {
|
|
264
335
|
return;
|
|
265
336
|
}
|
|
@@ -281,25 +352,52 @@ export class AuthStorage {
|
|
|
281
352
|
}
|
|
282
353
|
|
|
283
354
|
/**
|
|
284
|
-
* Get credential for a provider.
|
|
355
|
+
* Get the first credential for a provider (backward-compatible).
|
|
285
356
|
*/
|
|
286
357
|
get(provider: string): AuthCredential | undefined {
|
|
287
|
-
|
|
358
|
+
const creds = this.getCredentialsForProvider(provider);
|
|
359
|
+
return creds[0] ?? undefined;
|
|
288
360
|
}
|
|
289
361
|
|
|
290
362
|
/**
|
|
291
|
-
* Set credential for a provider.
|
|
363
|
+
* Set credential for a provider. For API key credentials, appends to
|
|
364
|
+
* existing credentials (accumulation on duplicate login). For OAuth,
|
|
365
|
+
* replaces (only one OAuth token per provider makes sense).
|
|
292
366
|
*/
|
|
293
367
|
set(provider: string, credential: AuthCredential): void {
|
|
294
|
-
|
|
295
|
-
|
|
368
|
+
if (credential.type === "api_key") {
|
|
369
|
+
const existing = this.getCredentialsForProvider(provider);
|
|
370
|
+
// Deduplicate: don't add if same key already exists
|
|
371
|
+
const isDuplicate = existing.some(
|
|
372
|
+
(c) => c.type === "api_key" && c.key === credential.key,
|
|
373
|
+
);
|
|
374
|
+
if (isDuplicate) return;
|
|
375
|
+
|
|
376
|
+
const updated = [...existing, credential];
|
|
377
|
+
this.data[provider] = updated.length === 1 ? updated[0] : updated;
|
|
378
|
+
this.persistProviderChange(provider, updated.length === 1 ? updated[0] : updated);
|
|
379
|
+
} else {
|
|
380
|
+
// OAuth: replace any existing OAuth credential, keep API keys
|
|
381
|
+
const existing = this.getCredentialsForProvider(provider);
|
|
382
|
+
const apiKeys = existing.filter((c) => c.type === "api_key");
|
|
383
|
+
if (apiKeys.length === 0) {
|
|
384
|
+
this.data[provider] = credential;
|
|
385
|
+
this.persistProviderChange(provider, credential);
|
|
386
|
+
} else {
|
|
387
|
+
const updated = [...apiKeys, credential];
|
|
388
|
+
this.data[provider] = updated;
|
|
389
|
+
this.persistProviderChange(provider, updated);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
296
392
|
}
|
|
297
393
|
|
|
298
394
|
/**
|
|
299
|
-
* Remove
|
|
395
|
+
* Remove all credentials for a provider.
|
|
300
396
|
*/
|
|
301
397
|
remove(provider: string): void {
|
|
302
398
|
delete this.data[provider];
|
|
399
|
+
this.providerRoundRobinIndex.delete(provider);
|
|
400
|
+
this.credentialBackoff.delete(provider);
|
|
303
401
|
this.persistProviderChange(provider, undefined);
|
|
304
402
|
}
|
|
305
403
|
|
|
@@ -331,9 +429,19 @@ export class AuthStorage {
|
|
|
331
429
|
|
|
332
430
|
/**
|
|
333
431
|
* Get all credentials (for passing to getOAuthApiKey).
|
|
432
|
+
* Returns normalized format where each provider has a single credential
|
|
433
|
+
* (the first one) for backward compatibility with OAuth refresh.
|
|
434
|
+
*
|
|
435
|
+
* NOTE: For providers with multiple API keys, only the first credential is
|
|
436
|
+
* returned. This is intentional — callers use this for OAuth refresh only,
|
|
437
|
+
* which is always single-credential. Do not use for API key enumeration.
|
|
334
438
|
*/
|
|
335
|
-
getAll():
|
|
336
|
-
|
|
439
|
+
getAll(): Record<string, AuthCredential> {
|
|
440
|
+
const result: Record<string, AuthCredential> = {};
|
|
441
|
+
for (const [provider, entry] of Object.entries(this.data)) {
|
|
442
|
+
result[provider] = Array.isArray(entry) ? entry[0] : entry;
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
337
445
|
}
|
|
338
446
|
|
|
339
447
|
drainErrors(): Error[] {
|
|
@@ -362,6 +470,108 @@ export class AuthStorage {
|
|
|
362
470
|
this.remove(provider);
|
|
363
471
|
}
|
|
364
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Check if a credential index is currently backed off.
|
|
475
|
+
*/
|
|
476
|
+
private isCredentialBackedOff(provider: string, index: number): boolean {
|
|
477
|
+
const providerBackoff = this.credentialBackoff.get(provider);
|
|
478
|
+
if (!providerBackoff) return false;
|
|
479
|
+
const expiresAt = providerBackoff.get(index);
|
|
480
|
+
if (expiresAt === undefined) return false;
|
|
481
|
+
if (Date.now() >= expiresAt) {
|
|
482
|
+
providerBackoff.delete(index);
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Select the best credential index for a provider.
|
|
490
|
+
* - If sessionId is provided, uses session-sticky hashing as the starting point.
|
|
491
|
+
* - Otherwise, uses round-robin as the starting point.
|
|
492
|
+
* - Skips credentials that are currently backed off.
|
|
493
|
+
* - Returns -1 if all credentials are backed off.
|
|
494
|
+
*/
|
|
495
|
+
private selectCredentialIndex(provider: string, credentials: AuthCredential[], sessionId?: string): number {
|
|
496
|
+
if (credentials.length === 0) return -1;
|
|
497
|
+
if (credentials.length === 1) {
|
|
498
|
+
return this.isCredentialBackedOff(provider, 0) ? -1 : 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let startIndex: number;
|
|
502
|
+
if (sessionId) {
|
|
503
|
+
startIndex = hashString(sessionId) % credentials.length;
|
|
504
|
+
} else {
|
|
505
|
+
const current = this.providerRoundRobinIndex.get(provider) ?? 0;
|
|
506
|
+
startIndex = current % credentials.length;
|
|
507
|
+
this.providerRoundRobinIndex.set(provider, current + 1);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Try starting from the preferred index, wrapping around
|
|
511
|
+
for (let offset = 0; offset < credentials.length; offset++) {
|
|
512
|
+
const index = (startIndex + offset) % credentials.length;
|
|
513
|
+
if (!this.isCredentialBackedOff(provider, index)) {
|
|
514
|
+
return index;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// All credentials are backed off
|
|
519
|
+
return -1;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Mark a credential as rate-limited. Finds the credential that was most
|
|
524
|
+
* recently used for this provider+session and backs it off.
|
|
525
|
+
*
|
|
526
|
+
* @returns true if another credential is available (caller should retry),
|
|
527
|
+
* false if all credentials for this provider are backed off.
|
|
528
|
+
*/
|
|
529
|
+
markUsageLimitReached(
|
|
530
|
+
provider: string,
|
|
531
|
+
sessionId?: string,
|
|
532
|
+
options?: { errorType?: UsageLimitErrorType },
|
|
533
|
+
): boolean {
|
|
534
|
+
const credentials = this.getCredentialsForProvider(provider);
|
|
535
|
+
if (credentials.length === 0) return false;
|
|
536
|
+
|
|
537
|
+
const errorType = options?.errorType ?? "rate_limit";
|
|
538
|
+
const backoffMs = getBackoffDuration(errorType);
|
|
539
|
+
|
|
540
|
+
// Determine which credential was just used (same logic as selectCredentialIndex
|
|
541
|
+
// but without incrementing round-robin)
|
|
542
|
+
let usedIndex: number;
|
|
543
|
+
if (credentials.length === 1) {
|
|
544
|
+
usedIndex = 0;
|
|
545
|
+
} else if (sessionId) {
|
|
546
|
+
usedIndex = hashString(sessionId) % credentials.length;
|
|
547
|
+
} else {
|
|
548
|
+
// Round-robin was already incremented in getApiKey, so the last-used
|
|
549
|
+
// index is (current - 1). Note: in a concurrent scenario where another
|
|
550
|
+
// getApiKey call fires between the original request and this backoff call,
|
|
551
|
+
// we may back off the wrong credential index. This is acceptable because:
|
|
552
|
+
// (a) pi runs single-threaded event loop, (b) backing off the wrong key
|
|
553
|
+
// is safe — it self-heals when the backoff expires.
|
|
554
|
+
const current = this.providerRoundRobinIndex.get(provider) ?? 0;
|
|
555
|
+
usedIndex = ((current - 1) % credentials.length + credentials.length) % credentials.length;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Set backoff for this credential
|
|
559
|
+
let providerBackoff = this.credentialBackoff.get(provider);
|
|
560
|
+
if (!providerBackoff) {
|
|
561
|
+
providerBackoff = new Map();
|
|
562
|
+
this.credentialBackoff.set(provider, providerBackoff);
|
|
563
|
+
}
|
|
564
|
+
providerBackoff.set(usedIndex, Date.now() + backoffMs);
|
|
565
|
+
|
|
566
|
+
// Check if any credential is still available
|
|
567
|
+
for (let i = 0; i < credentials.length; i++) {
|
|
568
|
+
if (!this.isCredentialBackedOff(provider, i)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
365
575
|
/**
|
|
366
576
|
* Refresh OAuth token with backend locking to prevent race conditions.
|
|
367
577
|
* Multiple pi instances may try to refresh simultaneously when tokens expire.
|
|
@@ -379,8 +589,10 @@ export class AuthStorage {
|
|
|
379
589
|
this.data = currentData;
|
|
380
590
|
this.loadError = null;
|
|
381
591
|
|
|
382
|
-
|
|
383
|
-
|
|
592
|
+
// Find the OAuth credential for this provider
|
|
593
|
+
const creds = this.getCredentialsForProvider(providerId);
|
|
594
|
+
const cred = creds.find((c) => c.type === "oauth");
|
|
595
|
+
if (!cred || cred.type !== "oauth") {
|
|
384
596
|
return { result: null };
|
|
385
597
|
}
|
|
386
598
|
|
|
@@ -390,8 +602,9 @@ export class AuthStorage {
|
|
|
390
602
|
|
|
391
603
|
const oauthCreds: Record<string, OAuthCredentials> = {};
|
|
392
604
|
for (const [key, value] of Object.entries(currentData)) {
|
|
393
|
-
|
|
394
|
-
|
|
605
|
+
const first = Array.isArray(value) ? value.find((c) => c.type === "oauth") : value;
|
|
606
|
+
if (first?.type === "oauth") {
|
|
607
|
+
oauthCreds[key] = first;
|
|
395
608
|
}
|
|
396
609
|
}
|
|
397
610
|
|
|
@@ -400,9 +613,20 @@ export class AuthStorage {
|
|
|
400
613
|
return { result: null };
|
|
401
614
|
}
|
|
402
615
|
|
|
616
|
+
// Update the OAuth credential in-place within the array
|
|
617
|
+
const existingEntry = currentData[providerId];
|
|
618
|
+
const newOAuthCred: OAuthCredential = { type: "oauth", ...refreshed.newCredentials };
|
|
619
|
+
let updatedEntry: AuthCredential | AuthCredential[];
|
|
620
|
+
|
|
621
|
+
if (Array.isArray(existingEntry)) {
|
|
622
|
+
updatedEntry = existingEntry.map((c) => (c.type === "oauth" ? newOAuthCred : c));
|
|
623
|
+
} else {
|
|
624
|
+
updatedEntry = newOAuthCred;
|
|
625
|
+
}
|
|
626
|
+
|
|
403
627
|
const merged: AuthStorageData = {
|
|
404
628
|
...currentData,
|
|
405
|
-
[providerId]:
|
|
629
|
+
[providerId]: updatedEntry,
|
|
406
630
|
};
|
|
407
631
|
this.data = merged;
|
|
408
632
|
this.loadError = null;
|
|
@@ -413,65 +637,71 @@ export class AuthStorage {
|
|
|
413
637
|
}
|
|
414
638
|
|
|
415
639
|
/**
|
|
416
|
-
*
|
|
417
|
-
* Priority:
|
|
418
|
-
* 1. Runtime override (CLI --api-key)
|
|
419
|
-
* 2. API key from auth.json
|
|
420
|
-
* 3. OAuth token from auth.json (auto-refreshed with locking)
|
|
421
|
-
* 4. Environment variable
|
|
422
|
-
* 5. Fallback resolver (models.json custom providers)
|
|
640
|
+
* Resolve an API key from a single credential.
|
|
423
641
|
*/
|
|
424
|
-
async
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const cred = this.data[providerId];
|
|
432
|
-
|
|
433
|
-
if (cred?.type === "api_key") {
|
|
642
|
+
private async resolveCredentialApiKey(
|
|
643
|
+
providerId: string,
|
|
644
|
+
cred: AuthCredential,
|
|
645
|
+
): Promise<string | undefined> {
|
|
646
|
+
if (cred.type === "api_key") {
|
|
434
647
|
return resolveConfigValue(cred.key);
|
|
435
648
|
}
|
|
436
649
|
|
|
437
|
-
if (cred
|
|
650
|
+
if (cred.type === "oauth") {
|
|
438
651
|
const provider = getOAuthProvider(providerId);
|
|
439
|
-
if (!provider)
|
|
440
|
-
// Unknown OAuth provider, can't get API key
|
|
441
|
-
return undefined;
|
|
442
|
-
}
|
|
652
|
+
if (!provider) return undefined;
|
|
443
653
|
|
|
444
|
-
// Check if token needs refresh
|
|
445
654
|
const needsRefresh = Date.now() >= cred.expires;
|
|
446
|
-
|
|
447
655
|
if (needsRefresh) {
|
|
448
|
-
// Use locked refresh to prevent race conditions
|
|
449
656
|
try {
|
|
450
657
|
const result = await this.refreshOAuthTokenWithLock(providerId);
|
|
451
|
-
if (result)
|
|
452
|
-
return result.apiKey;
|
|
453
|
-
}
|
|
658
|
+
if (result) return result.apiKey;
|
|
454
659
|
} catch (error) {
|
|
455
660
|
this.recordError(error);
|
|
456
|
-
// Refresh failed - re-read file to check if another instance succeeded
|
|
457
661
|
this.reload();
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
return provider.getApiKey(updatedCred);
|
|
662
|
+
const updatedCreds = this.getCredentialsForProvider(providerId);
|
|
663
|
+
const updatedOAuth = updatedCreds.find((c) => c.type === "oauth");
|
|
664
|
+
if (updatedOAuth?.type === "oauth" && Date.now() < updatedOAuth.expires) {
|
|
665
|
+
return provider.getApiKey(updatedOAuth);
|
|
463
666
|
}
|
|
464
|
-
|
|
465
|
-
// Refresh truly failed - return undefined so model discovery skips this provider
|
|
466
|
-
// User can /login to re-authenticate (credentials preserved for retry)
|
|
467
667
|
return undefined;
|
|
468
668
|
}
|
|
469
669
|
} else {
|
|
470
|
-
// Token not expired, use current access token
|
|
471
670
|
return provider.getApiKey(cred);
|
|
472
671
|
}
|
|
473
672
|
}
|
|
474
673
|
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Get API key for a provider.
|
|
679
|
+
* Priority:
|
|
680
|
+
* 1. Runtime override (CLI --api-key)
|
|
681
|
+
* 2. Credential(s) from auth.json (with round-robin / session-sticky selection)
|
|
682
|
+
* 3. Environment variable
|
|
683
|
+
* 4. Fallback resolver (models.json custom providers)
|
|
684
|
+
*
|
|
685
|
+
* @param providerId - The provider to get an API key for
|
|
686
|
+
* @param sessionId - Optional session ID for sticky credential selection
|
|
687
|
+
*/
|
|
688
|
+
async getApiKey(providerId: string, sessionId?: string): Promise<string | undefined> {
|
|
689
|
+
// Runtime override takes highest priority
|
|
690
|
+
const runtimeKey = this.runtimeOverrides.get(providerId);
|
|
691
|
+
if (runtimeKey) {
|
|
692
|
+
return runtimeKey;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const credentials = this.getCredentialsForProvider(providerId);
|
|
696
|
+
|
|
697
|
+
if (credentials.length > 0) {
|
|
698
|
+
const index = this.selectCredentialIndex(providerId, credentials, sessionId);
|
|
699
|
+
if (index >= 0) {
|
|
700
|
+
return this.resolveCredentialApiKey(providerId, credentials[index]);
|
|
701
|
+
}
|
|
702
|
+
// All credentials backed off - fall through to env/fallback
|
|
703
|
+
}
|
|
704
|
+
|
|
475
705
|
// Fall back to environment variable
|
|
476
706
|
const envKey = getEnvApiKey(providerId);
|
|
477
707
|
if (envKey) return envKey;
|
|
@@ -517,16 +517,18 @@ export class ModelRegistry {
|
|
|
517
517
|
|
|
518
518
|
/**
|
|
519
519
|
* Get API key for a model.
|
|
520
|
+
* @param sessionId - Optional session ID for sticky credential selection
|
|
520
521
|
*/
|
|
521
|
-
async getApiKey(model: Model<Api
|
|
522
|
-
return this.authStorage.getApiKey(model.provider);
|
|
522
|
+
async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
|
|
523
|
+
return this.authStorage.getApiKey(model.provider, sessionId);
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
/**
|
|
526
527
|
* Get API key for a provider.
|
|
528
|
+
* @param sessionId - Optional session ID for sticky credential selection
|
|
527
529
|
*/
|
|
528
|
-
async getApiKeyForProvider(provider: string): Promise<string | undefined> {
|
|
529
|
-
return this.authStorage.getApiKey(provider);
|
|
530
|
+
async getApiKeyForProvider(provider: string, sessionId?: string): Promise<string | undefined> {
|
|
531
|
+
return this.authStorage.getApiKey(provider, sessionId);
|
|
530
532
|
}
|
|
531
533
|
|
|
532
534
|
/**
|
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
4
|
import lockfile from "proper-lockfile";
|
|
5
5
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
|
6
|
+
import type { BashInterceptorRule } from "./tools/bash-interceptor.js";
|
|
6
7
|
|
|
7
8
|
export interface CompactionSettings {
|
|
8
9
|
enabled?: boolean; // default: true
|
|
@@ -39,10 +40,20 @@ export interface ThinkingBudgetsSettings {
|
|
|
39
40
|
high?: number;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export interface BashInterceptorSettings {
|
|
44
|
+
enabled?: boolean; // default: true
|
|
45
|
+
rules?: BashInterceptorRule[]; // override default rules
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
export interface MarkdownSettings {
|
|
43
49
|
codeBlockIndent?: string; // default: " "
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
export interface AsyncSettings {
|
|
53
|
+
enabled?: boolean; // default: false
|
|
54
|
+
maxJobs?: number; // default: 100
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
export type TransportSetting = Transport;
|
|
47
58
|
|
|
48
59
|
/**
|
|
@@ -93,6 +104,8 @@ export interface Settings {
|
|
|
93
104
|
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
|
94
105
|
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
|
95
106
|
markdown?: MarkdownSettings;
|
|
107
|
+
async?: AsyncSettings;
|
|
108
|
+
bashInterceptor?: BashInterceptorSettings;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -357,6 +370,14 @@ export class SettingsManager {
|
|
|
357
370
|
return structuredClone(this.projectSettings);
|
|
358
371
|
}
|
|
359
372
|
|
|
373
|
+
getBashInterceptorEnabled(): boolean {
|
|
374
|
+
return this.settings.bashInterceptor?.enabled ?? true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getBashInterceptorRules(): BashInterceptorRule[] | undefined {
|
|
378
|
+
return this.settings.bashInterceptor?.rules;
|
|
379
|
+
}
|
|
380
|
+
|
|
360
381
|
reload(): void {
|
|
361
382
|
const globalLoad = SettingsManager.tryLoadFromStorage(this.storage, "global");
|
|
362
383
|
if (!globalLoad.error) {
|
|
@@ -939,4 +960,12 @@ export class SettingsManager {
|
|
|
939
960
|
getCodeBlockIndent(): string {
|
|
940
961
|
return this.settings.markdown?.codeBlockIndent ?? " ";
|
|
941
962
|
}
|
|
963
|
+
|
|
964
|
+
getAsyncEnabled(): boolean {
|
|
965
|
+
return this.settings.async?.enabled ?? false;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
getAsyncMaxJobs(): number {
|
|
969
|
+
return this.settings.async?.maxJobs ?? 100;
|
|
970
|
+
}
|
|
942
971
|
}
|