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.
Files changed (136) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +7 -0
  3. package/dist/loader.js +1 -0
  4. package/dist/onboarding.js +104 -59
  5. package/dist/update-cmd.d.ts +1 -0
  6. package/dist/update-cmd.js +40 -0
  7. package/node_modules/@gsd/native/dist/hasher/index.d.ts +32 -0
  8. package/node_modules/@gsd/native/dist/hasher/index.js +37 -0
  9. package/node_modules/@gsd/native/dist/native.d.ts +4 -1
  10. package/node_modules/@gsd/native/dist/native.js +39 -9
  11. package/node_modules/@gsd/native/dist/xxhash/index.d.ts +14 -0
  12. package/node_modules/@gsd/native/dist/xxhash/index.js +17 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js +58 -9
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js +254 -43
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js +6 -4
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js +12 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +18 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +1 -0
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  49. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -2
  50. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  51. package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/src/core/agent-session.ts +65 -9
  57. package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
  58. package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.ts +283 -53
  59. package/node_modules/@gsd/pi-coding-agent/src/core/model-registry.ts +6 -4
  60. package/node_modules/@gsd/pi-coding-agent/src/core/settings-manager.ts +29 -0
  61. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
  62. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
  63. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +29 -0
  64. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +8 -0
  65. package/node_modules/@gsd/pi-coding-agent/src/index.ts +6 -0
  66. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  67. package/package.json +8 -2
  68. package/packages/native/dist/hasher/index.d.ts +32 -0
  69. package/packages/native/dist/hasher/index.js +37 -0
  70. package/packages/native/dist/native.d.ts +4 -1
  71. package/packages/native/dist/native.js +39 -9
  72. package/packages/native/dist/xxhash/index.d.ts +14 -0
  73. package/packages/native/dist/xxhash/index.js +17 -0
  74. package/packages/native/src/native.ts +39 -9
  75. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
  76. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/agent-session.js +58 -9
  78. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
  80. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/auth-storage.js +254 -43
  82. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
  84. package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
  86. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
  88. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/model-registry.js +6 -4
  90. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
  92. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
  94. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
  96. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
  98. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
  102. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
  104. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/tools/bash.js +18 -0
  106. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  108. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
  110. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/index.d.ts +2 -2
  112. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/index.js +1 -1
  114. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  118. package/packages/pi-coding-agent/src/core/agent-session.ts +65 -9
  119. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
  120. package/packages/pi-coding-agent/src/core/auth-storage.ts +283 -53
  121. package/packages/pi-coding-agent/src/core/model-registry.ts +6 -4
  122. package/packages/pi-coding-agent/src/core/settings-manager.ts +29 -0
  123. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
  124. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
  125. package/packages/pi-coding-agent/src/core/tools/bash.ts +29 -0
  126. package/packages/pi-coding-agent/src/core/tools/index.ts +8 -0
  127. package/packages/pi-coding-agent/src/index.ts +6 -0
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  129. package/src/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
  130. package/src/resources/extensions/async-jobs/await-tool.ts +101 -0
  131. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
  132. package/src/resources/extensions/async-jobs/index.ts +133 -0
  133. package/src/resources/extensions/async-jobs/job-manager.ts +250 -0
  134. package/src/resources/extensions/gsd/git-service.ts +13 -3
  135. package/src/resources/extensions/gsd/prompts/system.md +5 -2
  136. 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
  */
@@ -14,7 +17,11 @@ export type OAuthCredential = {
14
17
  type: "oauth";
15
18
  } & OAuthCredentials;
16
19
  export type AuthCredential = ApiKeyCredential | OAuthCredential;
17
- export type AuthStorageData = Record<string, AuthCredential>;
20
+ /**
21
+ * On-disk format: each provider maps to a single credential or an array of credentials.
22
+ * Single credentials are normalized to arrays at load time for internal use.
23
+ */
24
+ export type AuthStorageData = Record<string, AuthCredential | AuthCredential[]>;
18
25
  type LockResult<T> = {
19
26
  result: T;
20
27
  next?: string;
@@ -37,8 +44,10 @@ export declare class InMemoryAuthStorageBackend implements AuthStorageBackend {
37
44
  withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
38
45
  withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
39
46
  }
47
+ export type UsageLimitErrorType = "rate_limit" | "quota_exhausted" | "server_error" | "unknown";
40
48
  /**
41
49
  * Credential storage backed by a JSON file.
50
+ * Supports multiple credentials per provider with round-robin rotation and rate-limit fallback.
42
51
  */
43
52
  export declare class AuthStorage {
44
53
  private storage;
@@ -47,6 +56,16 @@ export declare class AuthStorage {
47
56
  private fallbackResolver?;
48
57
  private loadError;
49
58
  private errors;
59
+ /**
60
+ * Round-robin index per provider. Incremented on each call to getApiKey
61
+ * when no sessionId is provided.
62
+ */
63
+ private providerRoundRobinIndex;
64
+ /**
65
+ * Backoff tracking per provider per credential index.
66
+ * Map<provider, Map<credentialIndex, backoffExpiresAt>>
67
+ */
68
+ private credentialBackoff;
50
69
  private constructor();
51
70
  static create(authPath?: string): AuthStorage;
52
71
  static fromStorage(storage: AuthStorageBackend): AuthStorage;
@@ -67,21 +86,28 @@ export declare class AuthStorage {
67
86
  setFallbackResolver(resolver: (provider: string) => string | undefined): void;
68
87
  private recordError;
69
88
  private parseStorageData;
89
+ /**
90
+ * Normalize a storage entry to an array of credentials.
91
+ * Handles both single credential (backward compat) and array formats.
92
+ */
93
+ getCredentialsForProvider(provider: string): AuthCredential[];
70
94
  /**
71
95
  * Reload credentials from storage.
72
96
  */
73
97
  reload(): void;
74
98
  private persistProviderChange;
75
99
  /**
76
- * Get credential for a provider.
100
+ * Get the first credential for a provider (backward-compatible).
77
101
  */
78
102
  get(provider: string): AuthCredential | undefined;
79
103
  /**
80
- * Set credential for a provider.
104
+ * Set credential for a provider. For API key credentials, appends to
105
+ * existing credentials (accumulation on duplicate login). For OAuth,
106
+ * replaces (only one OAuth token per provider makes sense).
81
107
  */
82
108
  set(provider: string, credential: AuthCredential): void;
83
109
  /**
84
- * Remove credential for a provider.
110
+ * Remove all credentials for a provider.
85
111
  */
86
112
  remove(provider: string): void;
87
113
  /**
@@ -99,8 +125,14 @@ export declare class AuthStorage {
99
125
  hasAuth(provider: string): boolean;
100
126
  /**
101
127
  * Get all credentials (for passing to getOAuthApiKey).
102
- */
103
- getAll(): AuthStorageData;
128
+ * Returns normalized format where each provider has a single credential
129
+ * (the first one) for backward compatibility with OAuth refresh.
130
+ *
131
+ * NOTE: For providers with multiple API keys, only the first credential is
132
+ * returned. This is intentional — callers use this for OAuth refresh only,
133
+ * which is always single-credential. Do not use for API key enumeration.
134
+ */
135
+ getAll(): Record<string, AuthCredential>;
104
136
  drainErrors(): Error[];
105
137
  /**
106
138
  * Login to an OAuth provider.
@@ -110,21 +142,49 @@ export declare class AuthStorage {
110
142
  * Logout from a provider.
111
143
  */
112
144
  logout(provider: string): void;
145
+ /**
146
+ * Check if a credential index is currently backed off.
147
+ */
148
+ private isCredentialBackedOff;
149
+ /**
150
+ * Select the best credential index for a provider.
151
+ * - If sessionId is provided, uses session-sticky hashing as the starting point.
152
+ * - Otherwise, uses round-robin as the starting point.
153
+ * - Skips credentials that are currently backed off.
154
+ * - Returns -1 if all credentials are backed off.
155
+ */
156
+ private selectCredentialIndex;
157
+ /**
158
+ * Mark a credential as rate-limited. Finds the credential that was most
159
+ * recently used for this provider+session and backs it off.
160
+ *
161
+ * @returns true if another credential is available (caller should retry),
162
+ * false if all credentials for this provider are backed off.
163
+ */
164
+ markUsageLimitReached(provider: string, sessionId?: string, options?: {
165
+ errorType?: UsageLimitErrorType;
166
+ }): boolean;
113
167
  /**
114
168
  * Refresh OAuth token with backend locking to prevent race conditions.
115
169
  * Multiple pi instances may try to refresh simultaneously when tokens expire.
116
170
  */
117
171
  private refreshOAuthTokenWithLock;
172
+ /**
173
+ * Resolve an API key from a single credential.
174
+ */
175
+ private resolveCredentialApiKey;
118
176
  /**
119
177
  * Get API key for a provider.
120
178
  * Priority:
121
179
  * 1. Runtime override (CLI --api-key)
122
- * 2. API key from auth.json
123
- * 3. OAuth token from auth.json (auto-refreshed with locking)
124
- * 4. Environment variable
125
- * 5. Fallback resolver (models.json custom providers)
126
- */
127
- getApiKey(providerId: string): Promise<string | undefined>;
180
+ * 2. Credential(s) from auth.json (with round-robin / session-sticky selection)
181
+ * 3. Environment variable
182
+ * 4. Fallback resolver (models.json custom providers)
183
+ *
184
+ * @param providerId - The provider to get an API key for
185
+ * @param sessionId - Optional session ID for sticky credential selection
186
+ */
187
+ getApiKey(providerId: string, sessionId?: string): Promise<string | undefined>;
128
188
  /**
129
189
  * Get all registered OAuth providers
130
190
  */
@@ -1 +1 @@
1
- {"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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"}
@@ -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
  */
@@ -137,8 +140,43 @@ export class InMemoryAuthStorageBackend {
137
140
  return result;
138
141
  }
139
142
  }
143
+ // ============================================================================
144
+ // Backoff durations for different error types (milliseconds)
145
+ // ============================================================================
146
+ const BACKOFF_RATE_LIMIT_MS = 30_000; // 30s for rate limit / 429
147
+ const BACKOFF_QUOTA_EXHAUSTED_MS = 30 * 60_000; // 30min for quota exhausted
148
+ const BACKOFF_SERVER_ERROR_MS = 20_000; // 20s for 5xx server errors
149
+ const BACKOFF_DEFAULT_MS = 60_000; // 60s fallback
150
+ /**
151
+ * Get backoff duration for an error type.
152
+ */
153
+ function getBackoffDuration(errorType) {
154
+ switch (errorType) {
155
+ case "rate_limit":
156
+ return BACKOFF_RATE_LIMIT_MS;
157
+ case "quota_exhausted":
158
+ return BACKOFF_QUOTA_EXHAUSTED_MS;
159
+ case "server_error":
160
+ return BACKOFF_SERVER_ERROR_MS;
161
+ default:
162
+ return BACKOFF_DEFAULT_MS;
163
+ }
164
+ }
165
+ /**
166
+ * Simple string hash for session-sticky credential selection.
167
+ * Returns a positive integer.
168
+ */
169
+ function hashString(str) {
170
+ let hash = 0;
171
+ for (let i = 0; i < str.length; i++) {
172
+ const char = str.charCodeAt(i);
173
+ hash = ((hash << 5) - hash + char) | 0;
174
+ }
175
+ return Math.abs(hash);
176
+ }
140
177
  /**
141
178
  * Credential storage backed by a JSON file.
179
+ * Supports multiple credentials per provider with round-robin rotation and rate-limit fallback.
142
180
  */
143
181
  export class AuthStorage {
144
182
  constructor(storage) {
@@ -147,6 +185,16 @@ export class AuthStorage {
147
185
  this.runtimeOverrides = new Map();
148
186
  this.loadError = null;
149
187
  this.errors = [];
188
+ /**
189
+ * Round-robin index per provider. Incremented on each call to getApiKey
190
+ * when no sessionId is provided.
191
+ */
192
+ this.providerRoundRobinIndex = new Map();
193
+ /**
194
+ * Backoff tracking per provider per credential index.
195
+ * Map<provider, Map<credentialIndex, backoffExpiresAt>>
196
+ */
197
+ this.credentialBackoff = new Map();
150
198
  this.reload();
151
199
  }
152
200
  static create(authPath) {
@@ -190,6 +238,18 @@ export class AuthStorage {
190
238
  }
191
239
  return JSON.parse(content);
192
240
  }
241
+ /**
242
+ * Normalize a storage entry to an array of credentials.
243
+ * Handles both single credential (backward compat) and array formats.
244
+ */
245
+ getCredentialsForProvider(provider) {
246
+ const entry = this.data[provider];
247
+ if (!entry)
248
+ return [];
249
+ if (Array.isArray(entry))
250
+ return entry;
251
+ return [entry];
252
+ }
193
253
  /**
194
254
  * Reload credentials from storage.
195
255
  */
@@ -230,23 +290,50 @@ export class AuthStorage {
230
290
  }
231
291
  }
232
292
  /**
233
- * Get credential for a provider.
293
+ * Get the first credential for a provider (backward-compatible).
234
294
  */
235
295
  get(provider) {
236
- return this.data[provider] ?? undefined;
296
+ const creds = this.getCredentialsForProvider(provider);
297
+ return creds[0] ?? undefined;
237
298
  }
238
299
  /**
239
- * Set credential for a provider.
300
+ * Set credential for a provider. For API key credentials, appends to
301
+ * existing credentials (accumulation on duplicate login). For OAuth,
302
+ * replaces (only one OAuth token per provider makes sense).
240
303
  */
241
304
  set(provider, credential) {
242
- this.data[provider] = credential;
243
- this.persistProviderChange(provider, credential);
305
+ if (credential.type === "api_key") {
306
+ const existing = this.getCredentialsForProvider(provider);
307
+ // Deduplicate: don't add if same key already exists
308
+ const isDuplicate = existing.some((c) => c.type === "api_key" && c.key === credential.key);
309
+ if (isDuplicate)
310
+ return;
311
+ const updated = [...existing, credential];
312
+ this.data[provider] = updated.length === 1 ? updated[0] : updated;
313
+ this.persistProviderChange(provider, updated.length === 1 ? updated[0] : updated);
314
+ }
315
+ else {
316
+ // OAuth: replace any existing OAuth credential, keep API keys
317
+ const existing = this.getCredentialsForProvider(provider);
318
+ const apiKeys = existing.filter((c) => c.type === "api_key");
319
+ if (apiKeys.length === 0) {
320
+ this.data[provider] = credential;
321
+ this.persistProviderChange(provider, credential);
322
+ }
323
+ else {
324
+ const updated = [...apiKeys, credential];
325
+ this.data[provider] = updated;
326
+ this.persistProviderChange(provider, updated);
327
+ }
328
+ }
244
329
  }
245
330
  /**
246
- * Remove credential for a provider.
331
+ * Remove all credentials for a provider.
247
332
  */
248
333
  remove(provider) {
249
334
  delete this.data[provider];
335
+ this.providerRoundRobinIndex.delete(provider);
336
+ this.credentialBackoff.delete(provider);
250
337
  this.persistProviderChange(provider, undefined);
251
338
  }
252
339
  /**
@@ -278,9 +365,19 @@ export class AuthStorage {
278
365
  }
279
366
  /**
280
367
  * Get all credentials (for passing to getOAuthApiKey).
368
+ * Returns normalized format where each provider has a single credential
369
+ * (the first one) for backward compatibility with OAuth refresh.
370
+ *
371
+ * NOTE: For providers with multiple API keys, only the first credential is
372
+ * returned. This is intentional — callers use this for OAuth refresh only,
373
+ * which is always single-credential. Do not use for API key enumeration.
281
374
  */
282
375
  getAll() {
283
- return { ...this.data };
376
+ const result = {};
377
+ for (const [provider, entry] of Object.entries(this.data)) {
378
+ result[provider] = Array.isArray(entry) ? entry[0] : entry;
379
+ }
380
+ return result;
284
381
  }
285
382
  drainErrors() {
286
383
  const drained = [...this.errors];
@@ -304,6 +401,101 @@ export class AuthStorage {
304
401
  logout(provider) {
305
402
  this.remove(provider);
306
403
  }
404
+ /**
405
+ * Check if a credential index is currently backed off.
406
+ */
407
+ isCredentialBackedOff(provider, index) {
408
+ const providerBackoff = this.credentialBackoff.get(provider);
409
+ if (!providerBackoff)
410
+ return false;
411
+ const expiresAt = providerBackoff.get(index);
412
+ if (expiresAt === undefined)
413
+ return false;
414
+ if (Date.now() >= expiresAt) {
415
+ providerBackoff.delete(index);
416
+ return false;
417
+ }
418
+ return true;
419
+ }
420
+ /**
421
+ * Select the best credential index for a provider.
422
+ * - If sessionId is provided, uses session-sticky hashing as the starting point.
423
+ * - Otherwise, uses round-robin as the starting point.
424
+ * - Skips credentials that are currently backed off.
425
+ * - Returns -1 if all credentials are backed off.
426
+ */
427
+ selectCredentialIndex(provider, credentials, sessionId) {
428
+ if (credentials.length === 0)
429
+ return -1;
430
+ if (credentials.length === 1) {
431
+ return this.isCredentialBackedOff(provider, 0) ? -1 : 0;
432
+ }
433
+ let startIndex;
434
+ if (sessionId) {
435
+ startIndex = hashString(sessionId) % credentials.length;
436
+ }
437
+ else {
438
+ const current = this.providerRoundRobinIndex.get(provider) ?? 0;
439
+ startIndex = current % credentials.length;
440
+ this.providerRoundRobinIndex.set(provider, current + 1);
441
+ }
442
+ // Try starting from the preferred index, wrapping around
443
+ for (let offset = 0; offset < credentials.length; offset++) {
444
+ const index = (startIndex + offset) % credentials.length;
445
+ if (!this.isCredentialBackedOff(provider, index)) {
446
+ return index;
447
+ }
448
+ }
449
+ // All credentials are backed off
450
+ return -1;
451
+ }
452
+ /**
453
+ * Mark a credential as rate-limited. Finds the credential that was most
454
+ * recently used for this provider+session and backs it off.
455
+ *
456
+ * @returns true if another credential is available (caller should retry),
457
+ * false if all credentials for this provider are backed off.
458
+ */
459
+ markUsageLimitReached(provider, sessionId, options) {
460
+ const credentials = this.getCredentialsForProvider(provider);
461
+ if (credentials.length === 0)
462
+ return false;
463
+ const errorType = options?.errorType ?? "rate_limit";
464
+ const backoffMs = getBackoffDuration(errorType);
465
+ // Determine which credential was just used (same logic as selectCredentialIndex
466
+ // but without incrementing round-robin)
467
+ let usedIndex;
468
+ if (credentials.length === 1) {
469
+ usedIndex = 0;
470
+ }
471
+ else if (sessionId) {
472
+ usedIndex = hashString(sessionId) % credentials.length;
473
+ }
474
+ else {
475
+ // Round-robin was already incremented in getApiKey, so the last-used
476
+ // index is (current - 1). Note: in a concurrent scenario where another
477
+ // getApiKey call fires between the original request and this backoff call,
478
+ // we may back off the wrong credential index. This is acceptable because:
479
+ // (a) pi runs single-threaded event loop, (b) backing off the wrong key
480
+ // is safe — it self-heals when the backoff expires.
481
+ const current = this.providerRoundRobinIndex.get(provider) ?? 0;
482
+ usedIndex = ((current - 1) % credentials.length + credentials.length) % credentials.length;
483
+ }
484
+ // Set backoff for this credential
485
+ let providerBackoff = this.credentialBackoff.get(provider);
486
+ if (!providerBackoff) {
487
+ providerBackoff = new Map();
488
+ this.credentialBackoff.set(provider, providerBackoff);
489
+ }
490
+ providerBackoff.set(usedIndex, Date.now() + backoffMs);
491
+ // Check if any credential is still available
492
+ for (let i = 0; i < credentials.length; i++) {
493
+ if (!this.isCredentialBackedOff(provider, i)) {
494
+ return true;
495
+ }
496
+ }
497
+ return false;
498
+ }
307
499
  /**
308
500
  * Refresh OAuth token with backend locking to prevent race conditions.
309
501
  * Multiple pi instances may try to refresh simultaneously when tokens expire.
@@ -317,8 +509,10 @@ export class AuthStorage {
317
509
  const currentData = this.parseStorageData(current);
318
510
  this.data = currentData;
319
511
  this.loadError = null;
320
- const cred = currentData[providerId];
321
- if (cred?.type !== "oauth") {
512
+ // Find the OAuth credential for this provider
513
+ const creds = this.getCredentialsForProvider(providerId);
514
+ const cred = creds.find((c) => c.type === "oauth");
515
+ if (!cred || cred.type !== "oauth") {
322
516
  return { result: null };
323
517
  }
324
518
  if (Date.now() < cred.expires) {
@@ -326,17 +520,28 @@ export class AuthStorage {
326
520
  }
327
521
  const oauthCreds = {};
328
522
  for (const [key, value] of Object.entries(currentData)) {
329
- if (value.type === "oauth") {
330
- oauthCreds[key] = value;
523
+ const first = Array.isArray(value) ? value.find((c) => c.type === "oauth") : value;
524
+ if (first?.type === "oauth") {
525
+ oauthCreds[key] = first;
331
526
  }
332
527
  }
333
528
  const refreshed = await getOAuthApiKey(providerId, oauthCreds);
334
529
  if (!refreshed) {
335
530
  return { result: null };
336
531
  }
532
+ // Update the OAuth credential in-place within the array
533
+ const existingEntry = currentData[providerId];
534
+ const newOAuthCred = { type: "oauth", ...refreshed.newCredentials };
535
+ let updatedEntry;
536
+ if (Array.isArray(existingEntry)) {
537
+ updatedEntry = existingEntry.map((c) => (c.type === "oauth" ? newOAuthCred : c));
538
+ }
539
+ else {
540
+ updatedEntry = newOAuthCred;
541
+ }
337
542
  const merged = {
338
543
  ...currentData,
339
- [providerId]: { type: "oauth", ...refreshed.newCredentials },
544
+ [providerId]: updatedEntry,
340
545
  };
341
546
  this.data = merged;
342
547
  this.loadError = null;
@@ -345,59 +550,65 @@ export class AuthStorage {
345
550
  return result;
346
551
  }
347
552
  /**
348
- * Get API key for a provider.
349
- * Priority:
350
- * 1. Runtime override (CLI --api-key)
351
- * 2. API key from auth.json
352
- * 3. OAuth token from auth.json (auto-refreshed with locking)
353
- * 4. Environment variable
354
- * 5. Fallback resolver (models.json custom providers)
553
+ * Resolve an API key from a single credential.
355
554
  */
356
- async getApiKey(providerId) {
357
- // Runtime override takes highest priority
358
- const runtimeKey = this.runtimeOverrides.get(providerId);
359
- if (runtimeKey) {
360
- return runtimeKey;
361
- }
362
- const cred = this.data[providerId];
363
- if (cred?.type === "api_key") {
555
+ async resolveCredentialApiKey(providerId, cred) {
556
+ if (cred.type === "api_key") {
364
557
  return resolveConfigValue(cred.key);
365
558
  }
366
- if (cred?.type === "oauth") {
559
+ if (cred.type === "oauth") {
367
560
  const provider = getOAuthProvider(providerId);
368
- if (!provider) {
369
- // Unknown OAuth provider, can't get API key
561
+ if (!provider)
370
562
  return undefined;
371
- }
372
- // Check if token needs refresh
373
563
  const needsRefresh = Date.now() >= cred.expires;
374
564
  if (needsRefresh) {
375
- // Use locked refresh to prevent race conditions
376
565
  try {
377
566
  const result = await this.refreshOAuthTokenWithLock(providerId);
378
- if (result) {
567
+ if (result)
379
568
  return result.apiKey;
380
- }
381
569
  }
382
570
  catch (error) {
383
571
  this.recordError(error);
384
- // Refresh failed - re-read file to check if another instance succeeded
385
572
  this.reload();
386
- const updatedCred = this.data[providerId];
387
- if (updatedCred?.type === "oauth" && Date.now() < updatedCred.expires) {
388
- // Another instance refreshed successfully, use those credentials
389
- return provider.getApiKey(updatedCred);
573
+ const updatedCreds = this.getCredentialsForProvider(providerId);
574
+ const updatedOAuth = updatedCreds.find((c) => c.type === "oauth");
575
+ if (updatedOAuth?.type === "oauth" && Date.now() < updatedOAuth.expires) {
576
+ return provider.getApiKey(updatedOAuth);
390
577
  }
391
- // Refresh truly failed - return undefined so model discovery skips this provider
392
- // User can /login to re-authenticate (credentials preserved for retry)
393
578
  return undefined;
394
579
  }
395
580
  }
396
581
  else {
397
- // Token not expired, use current access token
398
582
  return provider.getApiKey(cred);
399
583
  }
400
584
  }
585
+ return undefined;
586
+ }
587
+ /**
588
+ * Get API key for a provider.
589
+ * Priority:
590
+ * 1. Runtime override (CLI --api-key)
591
+ * 2. Credential(s) from auth.json (with round-robin / session-sticky selection)
592
+ * 3. Environment variable
593
+ * 4. Fallback resolver (models.json custom providers)
594
+ *
595
+ * @param providerId - The provider to get an API key for
596
+ * @param sessionId - Optional session ID for sticky credential selection
597
+ */
598
+ async getApiKey(providerId, sessionId) {
599
+ // Runtime override takes highest priority
600
+ const runtimeKey = this.runtimeOverrides.get(providerId);
601
+ if (runtimeKey) {
602
+ return runtimeKey;
603
+ }
604
+ const credentials = this.getCredentialsForProvider(providerId);
605
+ if (credentials.length > 0) {
606
+ const index = this.selectCredentialIndex(providerId, credentials, sessionId);
607
+ if (index >= 0) {
608
+ return this.resolveCredentialApiKey(providerId, credentials[index]);
609
+ }
610
+ // All credentials backed off - fall through to env/fallback
611
+ }
401
612
  // Fall back to environment variable
402
613
  const envKey = getEnvApiKey(providerId);
403
614
  if (envKey)