@linxiraos/pi-ai 1.1.26 → 1.2.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.
Files changed (237) hide show
  1. package/CHANGELOG.md +9 -8
  2. package/README.md +15 -0
  3. package/dist/types/auth/affinity.d.ts +33 -6
  4. package/dist/types/auth/cascade.d.ts +44 -7
  5. package/dist/types/auth/oauth.d.ts +9 -6
  6. package/dist/types/auth/policy.d.ts +1 -0
  7. package/dist/types/auth/refresh.d.ts +1 -2
  8. package/dist/types/auth/select.d.ts +6 -4
  9. package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
  10. package/dist/types/auth/store.d.ts +8 -2
  11. package/dist/types/auth/types.d.ts +99 -15
  12. package/dist/types/auth-broker/client.d.ts +3 -3
  13. package/dist/types/auth-broker/discover.d.ts +1 -1
  14. package/dist/types/auth-broker/refresher.d.ts +6 -1
  15. package/dist/types/auth-broker/remote-store.d.ts +6 -4
  16. package/dist/types/auth-broker/server.d.ts +2 -0
  17. package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
  18. package/dist/types/auth-gateway/dispatch.d.ts +13 -5
  19. package/dist/types/auth-gateway/http.d.ts +0 -2
  20. package/dist/types/auth-gateway/index.d.ts +1 -0
  21. package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
  22. package/dist/types/auth-gateway/routes/images.d.ts +3 -3
  23. package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
  24. package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
  25. package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
  26. package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
  27. package/dist/types/auth-gateway/routes/video.d.ts +4 -4
  28. package/dist/types/auth-gateway/server.d.ts +22 -5
  29. package/dist/types/auth-gateway/session-state.d.ts +6 -0
  30. package/dist/types/auth-gateway/stdio.d.ts +12 -0
  31. package/dist/types/auth-storage.d.ts +7 -6
  32. package/dist/types/dialect/terminator-wait.d.ts +19 -0
  33. package/dist/types/env-api-key.d.ts +21 -0
  34. package/dist/types/error/auth.d.ts +10 -0
  35. package/dist/types/error/flags.d.ts +38 -0
  36. package/dist/types/images/index.d.ts +2 -3
  37. package/dist/types/images/shared.d.ts +6 -0
  38. package/dist/types/index.d.ts +2 -1
  39. package/dist/types/judgment/index.d.ts +2 -1
  40. package/dist/types/judgment/native.d.ts +80 -0
  41. package/dist/types/judgment/openai-decisions.d.ts +3 -0
  42. package/dist/types/judgment/question-file.d.ts +52 -0
  43. package/dist/types/judgment/systemone.d.ts +7 -0
  44. package/dist/types/judgment/types.d.ts +2 -2
  45. package/dist/types/judgment/wire.d.ts +36 -0
  46. package/dist/types/provider-details.d.ts +1 -1
  47. package/dist/types/provider-session-state.d.ts +2 -2
  48. package/dist/types/providers/anthropic-client.d.ts +10 -0
  49. package/dist/types/providers/anthropic-state.d.ts +2 -0
  50. package/dist/types/providers/anthropic.d.ts +38 -3
  51. package/dist/types/providers/aws-credentials.d.ts +4 -8
  52. package/dist/types/providers/connect-error-detail.d.ts +9 -0
  53. package/dist/types/providers/connect-frame.d.ts +44 -0
  54. package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
  55. package/dist/types/providers/cursor.d.ts +8 -8
  56. package/dist/types/providers/google-auth.d.ts +4 -1
  57. package/dist/types/providers/google-shared.d.ts +24 -4
  58. package/dist/types/providers/inference-headers.d.ts +6 -0
  59. package/dist/types/providers/mock.d.ts +3 -1
  60. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
  61. package/dist/types/providers/openai-codex-responses.d.ts +1 -1
  62. package/dist/types/providers/openai-configuration-update.d.ts +20 -11
  63. package/dist/types/providers/openai-responses-wire.d.ts +1 -55
  64. package/dist/types/providers/openai-responses.d.ts +40 -3
  65. package/dist/types/providers/openai-shared.d.ts +30 -5
  66. package/dist/types/providers/pi-native-client.d.ts +1 -1
  67. package/dist/types/providers/transform-messages.d.ts +25 -0
  68. package/dist/types/providers/vision-guard.d.ts +1 -1
  69. package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
  70. package/dist/types/registry/oauth/cursor.d.ts +10 -2
  71. package/dist/types/registry/oauth/index.d.ts +2 -0
  72. package/dist/types/registry/oauth/snowflake.d.ts +3 -0
  73. package/dist/types/registry/snowflake.d.ts +4 -0
  74. package/dist/types/stream.d.ts +0 -21
  75. package/dist/types/transcription/types.d.ts +2 -1
  76. package/dist/types/types.d.ts +116 -14
  77. package/dist/types/usage.d.ts +2 -0
  78. package/dist/types/utils/anthropic-auth.d.ts +21 -7
  79. package/dist/types/utils/aws-profile.d.ts +5 -0
  80. package/dist/types/utils/block-symbols.d.ts +14 -0
  81. package/dist/types/utils/dsml-leak.d.ts +29 -0
  82. package/dist/types/utils/glyph-codec.d.ts +1 -1
  83. package/dist/types/utils/idle-iterator.d.ts +11 -0
  84. package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
  85. package/dist/types/utils/resolve-peer.d.ts +2 -0
  86. package/dist/types/utils/retry-wait.d.ts +9 -0
  87. package/dist/types/utils/schema/normalize.d.ts +8 -3
  88. package/dist/types/utils/stream-markup-healing.d.ts +4 -4
  89. package/dist/types/utils/tool-call-arguments.d.ts +8 -0
  90. package/dist/types/utils.d.ts +6 -0
  91. package/package.json +6 -6
  92. package/src/auth/affinity.ts +130 -37
  93. package/src/auth/blocks.ts +94 -15
  94. package/src/auth/cascade.ts +112 -17
  95. package/src/auth/health.ts +3 -2
  96. package/src/auth/oauth.ts +25 -10
  97. package/src/auth/policy.ts +10 -1
  98. package/src/auth/pool.ts +0 -5
  99. package/src/auth/refresh.ts +17 -12
  100. package/src/auth/resets.ts +1 -1
  101. package/src/auth/rotation.ts +8 -2
  102. package/src/auth/select.ts +102 -30
  103. package/src/auth/sqlite-credential-store.ts +119 -49
  104. package/src/auth/store.ts +8 -2
  105. package/src/auth/types.ts +108 -17
  106. package/src/auth/usage-cache.ts +16 -5
  107. package/src/auth/usage.ts +2 -2
  108. package/src/auth-broker/client.ts +7 -4
  109. package/src/auth-broker/discover.ts +6 -5
  110. package/src/auth-broker/refresher.ts +8 -1
  111. package/src/auth-broker/remote-store.ts +200 -62
  112. package/src/auth-broker/server.ts +316 -130
  113. package/src/auth-broker/snapshot-cache.ts +142 -2
  114. package/src/auth-gateway/dispatch.ts +15 -7
  115. package/src/auth-gateway/http.ts +0 -8
  116. package/src/auth-gateway/index.ts +1 -0
  117. package/src/auth-gateway/routes/embeddings.ts +2 -2
  118. package/src/auth-gateway/routes/images.ts +4 -4
  119. package/src/auth-gateway/routes/rerank.ts +2 -2
  120. package/src/auth-gateway/routes/speech.ts +2 -2
  121. package/src/auth-gateway/routes/systemone.ts +22 -14
  122. package/src/auth-gateway/routes/transcriptions.ts +2 -2
  123. package/src/auth-gateway/routes/video.ts +7 -7
  124. package/src/auth-gateway/server.ts +175 -134
  125. package/src/auth-gateway/session-state.ts +51 -9
  126. package/src/auth-gateway/stdio.ts +132 -0
  127. package/src/auth-storage.ts +23 -9
  128. package/src/dialect/deepseek.ts +67 -8
  129. package/src/dialect/gemini.ts +8 -2
  130. package/src/dialect/hermes.ts +8 -2
  131. package/src/dialect/kimi.ts +8 -2
  132. package/src/dialect/qwen-xml.ts +8 -2
  133. package/src/dialect/qwen3.ts +8 -2
  134. package/src/dialect/terminator-wait.ts +42 -0
  135. package/src/env-api-key.ts +78 -0
  136. package/src/error/auth.ts +20 -0
  137. package/src/error/flags.ts +143 -24
  138. package/src/error/rate-limit.ts +8 -6
  139. package/src/images/google-antigravity.ts +0 -1
  140. package/src/images/index.ts +3 -17
  141. package/src/images/openai-hosted.ts +13 -4
  142. package/src/images/openai-images.ts +19 -13
  143. package/src/images/shared.ts +10 -2
  144. package/src/index.ts +2 -1
  145. package/src/judgment/index.ts +2 -1
  146. package/src/judgment/native.ts +333 -0
  147. package/src/judgment/openai-decisions-predicate.md +9 -0
  148. package/src/judgment/openai-decisions.ts +129 -0
  149. package/src/judgment/question-file.ts +90 -0
  150. package/src/judgment/systemone.ts +65 -0
  151. package/src/judgment/text.ts +9 -1
  152. package/src/judgment/types.ts +2 -2
  153. package/src/judgment/wire.ts +42 -0
  154. package/src/provider-details.ts +1 -1
  155. package/src/provider-session-state.ts +2 -2
  156. package/src/providers/amazon-bedrock.ts +19 -8
  157. package/src/providers/anthropic-client.ts +16 -1
  158. package/src/providers/anthropic-state.ts +6 -0
  159. package/src/providers/anthropic.ts +962 -238
  160. package/src/providers/apple-foundation-models.ts +10 -4
  161. package/src/providers/aws-credentials.ts +65 -5
  162. package/src/providers/azure-openai-responses.ts +1 -1
  163. package/src/providers/connect-error-detail.ts +21 -0
  164. package/src/providers/connect-frame.ts +107 -0
  165. package/src/providers/cursor/interaction-query.ts +1 -9
  166. package/src/providers/cursor.ts +590 -133
  167. package/src/providers/devin.ts +68 -58
  168. package/src/providers/factory-droid/gemini.ts +38 -51
  169. package/src/providers/factory-droid.ts +2 -0
  170. package/src/providers/gitlab-duo-workflow.ts +5 -2
  171. package/src/providers/gitlab-duo.ts +1 -0
  172. package/src/providers/google-auth.ts +10 -3
  173. package/src/providers/google-gemini-cli.ts +4 -0
  174. package/src/providers/google-shared.ts +134 -105
  175. package/src/providers/google-vertex.ts +0 -1
  176. package/src/providers/google.ts +1 -1
  177. package/src/providers/inference-headers.ts +19 -0
  178. package/src/providers/mock.ts +4 -0
  179. package/src/providers/ollama.ts +1 -1
  180. package/src/providers/openai-anthropic-shim.ts +1 -0
  181. package/src/providers/openai-codex/request-transformer.ts +16 -11
  182. package/src/providers/openai-codex-responses.ts +201 -76
  183. package/src/providers/openai-completions.ts +12 -2
  184. package/src/providers/openai-configuration-update.ts +54 -15
  185. package/src/providers/openai-responses-wire.ts +1 -55
  186. package/src/providers/openai-responses.ts +437 -33
  187. package/src/providers/openai-shared.ts +267 -72
  188. package/src/providers/pi-native-client.ts +2 -2
  189. package/src/providers/pi-native-server.ts +1 -0
  190. package/src/providers/systemone-server.ts +1 -1
  191. package/src/providers/transcriptions-server.ts +1 -1
  192. package/src/providers/transform-messages.ts +240 -84
  193. package/src/providers/vision-guard.ts +1 -1
  194. package/src/registry/aws.ts +51 -14
  195. package/src/registry/cloudflare-ai-gateway.ts +26 -3
  196. package/src/registry/hooks/api-key.ts +1 -0
  197. package/src/registry/hooks/custom.ts +2 -0
  198. package/src/registry/hooks/env.ts +2 -5
  199. package/src/registry/oauth/callback-server.ts +6 -1
  200. package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
  201. package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
  202. package/src/registry/oauth/cursor.ts +80 -17
  203. package/src/registry/oauth/index.ts +5 -0
  204. package/src/registry/oauth/snowflake.ts +183 -0
  205. package/src/registry/registry.ts +2 -0
  206. package/src/registry/snowflake.ts +149 -0
  207. package/src/stream.ts +85 -107
  208. package/src/transcription/types.ts +2 -1
  209. package/src/types.ts +142 -15
  210. package/src/usage/claude.ts +19 -12
  211. package/src/usage/commandcode.ts +1 -1
  212. package/src/usage/cursor.ts +14 -25
  213. package/src/usage/github-copilot.ts +1 -1
  214. package/src/usage/kimi.ts +1 -1
  215. package/src/usage/minimax-code.ts +8 -1
  216. package/src/usage.ts +8 -0
  217. package/src/utils/anthropic-auth.ts +34 -10
  218. package/src/utils/aws-profile.ts +10 -4
  219. package/src/utils/block-symbols.ts +17 -0
  220. package/src/utils/dsml-leak.ts +109 -0
  221. package/src/utils/empty-completion-retry.ts +2 -3
  222. package/src/utils/glyph-codec.ts +200 -7
  223. package/src/utils/idle-iterator.ts +80 -35
  224. package/src/utils/leaked-thinking-stream.ts +47 -25
  225. package/src/utils/resolve-peer.ts +7 -0
  226. package/src/utils/retry-wait.ts +19 -0
  227. package/src/utils/schema/CONSTRAINTS.md +9 -7
  228. package/src/utils/schema/fields.ts +20 -0
  229. package/src/utils/schema/normalize.ts +65 -23
  230. package/src/utils/schema/wire.ts +3 -3
  231. package/src/utils/stream-markup-healing.ts +4 -4
  232. package/src/utils/thinking-loop.ts +36 -13
  233. package/src/utils/tool-call-arguments.ts +16 -0
  234. package/src/utils/transport-fetch.ts +1 -1
  235. package/src/utils.ts +41 -10
  236. package/dist/types/judgment/typesafe.d.ts +0 -49
  237. package/src/judgment/typesafe.ts +0 -183
@@ -64,6 +64,14 @@ export function credentialBlockScopesForRequest(
64
64
  */
65
65
  type CredentialBackoff = { until: number; timed: boolean; probeAfter: number; persisted?: boolean };
66
66
 
67
+ /**
68
+ * One read of a credential's persisted blocks under a providerKey, keyed by
69
+ * scope (`""` is unscoped). `undefined`: the store has no bulk read, so use
70
+ * per-scope point reads. `null`: unreadable (damaged store or failed read), so
71
+ * it neither contributes deadlines nor retires mirrored in-memory blocks.
72
+ */
73
+ type PersistedBlockScopes = ReadonlyMap<string, number> | null | undefined;
74
+
67
75
  /** Latch for unrecoverably corrupt persisted block stores, shared with the credential pool. */
68
76
  export class BlockStoreHealth {
69
77
  readonly #sourceLabel: string | undefined;
@@ -134,19 +142,37 @@ export class CredentialBlocks implements BlocksApi {
134
142
  }
135
143
  }
136
144
 
137
- /** Returns in-memory block expiry timestamp for a credential/key pair, cleaning up expired entries. */
138
- #getCredentialBlockedUntilForKey(backoffKey: string, credentialId: number, nowMs: number): number | undefined {
145
+ /**
146
+ * Returns in-memory block expiry timestamp for a credential/key pair, cleaning up expired entries.
147
+ * `persistedScopes` is a bulk read for the key's providerKey; omitted, the store is point-read.
148
+ */
149
+ #getCredentialBlockedUntilForKey(
150
+ backoffKey: string,
151
+ credentialId: number,
152
+ nowMs: number,
153
+ persistedScopes: PersistedBlockScopes = undefined,
154
+ ): number | undefined {
139
155
  const block = this.#credentialBackoff.get(backoffKey)?.get(credentialId);
140
156
  if (!block) return undefined;
141
157
  // Once mirrored successfully, the store is authoritative for cross-process
142
158
  // deletion. A reset in a sibling must also retire this process's old map entry.
143
- if (block.persisted && !this.#deps.health.damaged && this.#deps.store.getCredentialBlock) {
159
+ // Upserts are longest-wins, so a persisted row shorter than the deadline this
160
+ // process mirrored means the row was deleted and replaced: the old deadline
161
+ // was superseded and must not win the next longest-wins merge.
162
+ if (
163
+ block.persisted &&
164
+ !this.#deps.health.damaged &&
165
+ persistedScopes !== null &&
166
+ (persistedScopes !== undefined || this.#deps.store.getCredentialBlock)
167
+ ) {
144
168
  const separator = backoffKey.indexOf("\0");
145
169
  const providerKey = separator < 0 ? backoffKey : backoffKey.slice(0, separator);
146
170
  const scope = separator < 0 ? "" : backoffKey.slice(separator + 1);
147
171
  try {
148
- const persisted = this.#deps.store.getCredentialBlock(credentialId, providerKey, scope);
149
- if (persisted === undefined) {
172
+ const persisted = persistedScopes
173
+ ? persistedScopes.get(scope)
174
+ : this.#deps.store.getCredentialBlock?.(credentialId, providerKey, scope);
175
+ if (persisted === undefined || persisted < block.until) {
150
176
  this.#deleteCredentialBackoff(backoffKey, credentialId);
151
177
  return undefined;
152
178
  }
@@ -172,7 +198,9 @@ export class CredentialBlocks implements BlocksApi {
172
198
  credentialId: number,
173
199
  providerKey: string,
174
200
  blockScope: string | undefined,
201
+ persistedScopes: PersistedBlockScopes = undefined,
175
202
  ): number | undefined {
203
+ if (persistedScopes !== undefined) return persistedScopes?.get(blockScope ?? "");
176
204
  if (this.#deps.health.damaged) return undefined;
177
205
  const getCredentialBlock = this.#deps.store.getCredentialBlock?.bind(this.#deps.store);
178
206
  if (!getCredentialBlock) return undefined;
@@ -190,6 +218,20 @@ export class CredentialBlocks implements BlocksApi {
190
218
  }
191
219
  }
192
220
 
221
+ /** Every persisted scope of one credential/providerKey in a single store read, when supported. */
222
+ #readPersistedCredentialBlockScopes(credentialId: number, providerKey: string): PersistedBlockScopes {
223
+ const store = this.#deps.store;
224
+ if (!store.getCredentialBlockScopes) return undefined;
225
+ if (this.#deps.health.damaged) return null;
226
+ try {
227
+ return store.getCredentialBlockScopes(credentialId, providerKey);
228
+ } catch (err) {
229
+ if (this.#deps.health.handle(err)) return null;
230
+ logger.debug("Failed to read credential blocks from persistent store", { err, credentialId, providerKey });
231
+ return null;
232
+ }
233
+ }
234
+
193
235
  #readPersistedCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number {
194
236
  if (this.#deps.health.damaged) return 0;
195
237
  const getCredentialBlockReconcileAfter = this.#deps.store.getCredentialBlockReconcileAfter?.bind(
@@ -212,13 +254,19 @@ export class CredentialBlocks implements BlocksApi {
212
254
  }
213
255
  }
214
256
 
215
- #exactBlockedUntil(credentialId: number, providerKey: string, scope: string | undefined): number | undefined {
257
+ #exactBlockedUntil(
258
+ credentialId: number,
259
+ providerKey: string,
260
+ scope: string | undefined,
261
+ persistedScopes: PersistedBlockScopes = this.#readPersistedCredentialBlockScopes(credentialId, providerKey),
262
+ ): number | undefined {
216
263
  const memory = this.#getCredentialBlockedUntilForKey(
217
264
  scopedBackoffKey(providerKey, scope),
218
265
  credentialId,
219
266
  Date.now(),
267
+ persistedScopes,
220
268
  );
221
- const persisted = this.#readPersistedCredentialBlock(credentialId, providerKey, scope);
269
+ const persisted = this.#readPersistedCredentialBlock(credentialId, providerKey, scope, persistedScopes);
222
270
  return memory === undefined ? persisted : persisted === undefined ? memory : Math.max(memory, persisted);
223
271
  }
224
272
 
@@ -228,6 +276,22 @@ export class CredentialBlocks implements BlocksApi {
228
276
  providerKey: string,
229
277
  credentialIndex: number,
230
278
  blockScopeOrScopes: string | readonly string[] | undefined = undefined,
279
+ ): number | undefined {
280
+ const credentialId = this.#deps.pool.entries(provider)[credentialIndex]?.id;
281
+ if (credentialId === undefined) return undefined;
282
+ return this.#blockedUntilById(
283
+ credentialId,
284
+ providerKey,
285
+ blockScopeOrScopes,
286
+ this.#readPersistedCredentialBlockScopes(credentialId, providerKey),
287
+ );
288
+ }
289
+
290
+ #blockedUntilById(
291
+ credentialId: number,
292
+ providerKey: string,
293
+ blockScopeOrScopes: string | readonly string[] | undefined,
294
+ persistedScopes: PersistedBlockScopes,
231
295
  ): number | undefined {
232
296
  const nowMs = Date.now();
233
297
  // A request honours its own scope plus any legacy catch-all scope, so a
@@ -236,21 +300,25 @@ export class CredentialBlocks implements BlocksApi {
236
300
  ...PROTECTED_BLOCK_SCOPES,
237
301
  ...(typeof blockScopeOrScopes === "string" ? [blockScopeOrScopes] : (blockScopeOrScopes ?? [])),
238
302
  ].filter(scope => scope.length > 0);
239
- const credentialId = this.#deps.pool.entries(provider)[credentialIndex]?.id;
240
- if (credentialId === undefined) return undefined;
241
- let blockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialId, nowMs);
303
+ let blockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialId, nowMs, persistedScopes);
242
304
  for (const blockScope of scopes) {
243
305
  const scopedBlockedUntil = this.#getCredentialBlockedUntilForKey(
244
306
  scopedBackoffKey(providerKey, blockScope),
245
307
  credentialId,
246
308
  nowMs,
309
+ persistedScopes,
247
310
  );
248
311
  if (scopedBlockedUntil !== undefined && (blockedUntil === undefined || scopedBlockedUntil > blockedUntil)) {
249
312
  blockedUntil = scopedBlockedUntil;
250
313
  }
251
314
  }
252
315
 
253
- const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, "");
316
+ const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(
317
+ credentialId,
318
+ providerKey,
319
+ "",
320
+ persistedScopes,
321
+ );
254
322
  if (
255
323
  persistedGlobalBlockedUntil !== undefined &&
256
324
  (blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
@@ -258,7 +326,12 @@ export class CredentialBlocks implements BlocksApi {
258
326
  blockedUntil = persistedGlobalBlockedUntil;
259
327
  }
260
328
  for (const blockScope of scopes) {
261
- const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, blockScope);
329
+ const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(
330
+ credentialId,
331
+ providerKey,
332
+ blockScope,
333
+ persistedScopes,
334
+ );
262
335
  if (
263
336
  persistedScopedBlockedUntil !== undefined &&
264
337
  (blockedUntil === undefined || persistedScopedBlockedUntil > blockedUntil)
@@ -437,14 +510,20 @@ export class CredentialBlocks implements BlocksApi {
437
510
  if (!this.supportsHealing(provider)) return false;
438
511
  const credentialId = this.#deps.pool.entries(provider)[credentialIndex]?.id;
439
512
  if (credentialId === undefined) return false;
440
- if (PROTECTED_BLOCK_SCOPES.some(scope => this.#exactBlockedUntil(credentialId, providerKey, scope) !== undefined))
513
+ // One persisted read serves the protected-scope, global, and requested checks.
514
+ const persistedScopes = this.#readPersistedCredentialBlockScopes(credentialId, providerKey);
515
+ if (
516
+ PROTECTED_BLOCK_SCOPES.some(
517
+ scope => this.#exactBlockedUntil(credentialId, providerKey, scope, persistedScopes) !== undefined,
518
+ )
519
+ )
441
520
  return false;
442
521
  if (
443
522
  !this.#deps.strategies(provider)?.healsGlobalBlocks &&
444
- this.blockedUntil(provider, providerKey, credentialIndex) !== undefined
523
+ this.#blockedUntilById(credentialId, providerKey, undefined, persistedScopes) !== undefined
445
524
  )
446
525
  return false;
447
- return this.blockedUntil(provider, providerKey, credentialIndex, blockScopeOrScopes) !== undefined;
526
+ return this.#blockedUntilById(credentialId, providerKey, blockScopeOrScopes, persistedScopes) !== undefined;
448
527
  }
449
528
 
450
529
  /**
@@ -1,13 +1,13 @@
1
1
  import { authPolicyFor } from "@linxiraos/pi-catalog/compat/auth";
2
- import { $env, $envExact } from "@linxiraos/pi-utils";
2
+ import { $env, $envExact, logger } from "@linxiraos/pi-utils";
3
3
  import { type ApiKeyResolver, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
4
+ import { getEnvApiKey, getEnvApiKeyName } from "../env-api-key";
4
5
  import * as AIError from "../error";
5
6
  import { isUsageLimitOutcome } from "../error/rate-limit";
6
7
  import { AUTHENTICATED_SENTINEL } from "../registry/types";
7
- import { getEnvApiKey, getEnvApiKeyName } from "../stream";
8
8
  import type { SessionAffinity } from "./affinity";
9
9
  import type { CredentialPool } from "./pool";
10
- import type { CredentialSelector } from "./select";
10
+ import type { CredentialSelector, OAuthResolutionResult } from "./select";
11
11
  import type {
12
12
  AuthApiKeyOptions,
13
13
  AuthCredential,
@@ -32,6 +32,7 @@ export class KeyOverrides {
32
32
  #runtimeOverrides: Map<string, string> = new Map();
33
33
  #configOverrides: Map<string, string> = new Map();
34
34
  #configFallbacks: Map<string, string> = new Map();
35
+ #mirrorKeys: Map<string, string> = new Map();
35
36
  #configValueResolver: (config: string) => Promise<string | undefined>;
36
37
 
37
38
  constructor(resolver?: (config: string) => Promise<string | undefined>) {
@@ -42,6 +43,17 @@ export class KeyOverrides {
42
43
  return this.#runtimeOverrides.has(provider) || this.#configOverrides.has(provider);
43
44
  }
44
45
 
46
+ /**
47
+ * Whether an override replaces OAuth for a session. Any override does for an
48
+ * unrestricted session. A restricted session skips the runtime key, but a
49
+ * config key still blocks OAuth: it marks the provider's endpoint (often a
50
+ * proxy) as taking that key, so the session fails closed instead of sending
51
+ * a pooled OAuth token there (see `KeyCascade.get`).
52
+ */
53
+ suppressesOAuth(provider: string, restricted: boolean): boolean {
54
+ return restricted ? this.#configOverrides.has(provider) : this.has(provider);
55
+ }
56
+
45
57
  runtimeKey(provider: string): string | undefined {
46
58
  return this.#runtimeOverrides.get(provider);
47
59
  }
@@ -55,6 +67,16 @@ export class KeyOverrides {
55
67
  return this.#configFallbacks.get(provider);
56
68
  }
57
69
 
70
+ /**
71
+ * Config value consulted only after every other source, including stored
72
+ * api_key credentials — the lowest cascade tier. The OMP compatibility
73
+ * overlay uses it so an upstream key is a pure fallback: any credential
74
+ * the user later stores locally wins without needing to evict the mirror.
75
+ */
76
+ mirrorKey(provider: string): string | undefined {
77
+ return this.#mirrorKeys.get(provider);
78
+ }
79
+
58
80
  /** Resolve a config value (env var name, "!command", literal) to the secret. */
59
81
  resolve(config: string): Promise<string | undefined> {
60
82
  return this.#configValueResolver(config);
@@ -89,8 +111,19 @@ export class KeyOverrides {
89
111
  * credentials (at the env-var tier). Providers that own a `/login` flow use
90
112
  * this so their default key reference (e.g. an unset env-var name, which
91
113
  * resolves to its literal text) cannot shadow the key the user logged in with.
114
+ *
115
+ * `mirror: true` ranks the value below everything else, under stored
116
+ * api_key credentials — a read-only compatibility mirror (upstream OMP
117
+ * agent.db key) that must never shadow a key the user stores locally.
92
118
  */
93
- setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void {
119
+ setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean; mirror?: boolean }): void {
120
+ if (options?.mirror) {
121
+ this.#configOverrides.delete(provider);
122
+ this.#configFallbacks.delete(provider);
123
+ this.#mirrorKeys.set(provider, apiKeyConfig);
124
+ return;
125
+ }
126
+ this.#mirrorKeys.delete(provider);
94
127
  if (options?.fallback) {
95
128
  this.#configOverrides.delete(provider);
96
129
  this.#configFallbacks.set(provider, apiKeyConfig);
@@ -101,11 +134,12 @@ export class KeyOverrides {
101
134
  }
102
135
 
103
136
  /**
104
- * Remove a single config-sourced API key (override or fallback).
137
+ * Remove a single config-sourced API key (override, fallback, or mirror).
105
138
  */
106
139
  removeConfig(provider: string): void {
107
140
  this.#configOverrides.delete(provider);
108
141
  this.#configFallbacks.delete(provider);
142
+ this.#mirrorKeys.delete(provider);
109
143
  }
110
144
 
111
145
  /**
@@ -115,6 +149,7 @@ export class KeyOverrides {
115
149
  clearConfig(): void {
116
150
  this.#configOverrides.clear();
117
151
  this.#configFallbacks.clear();
152
+ this.#mirrorKeys.clear();
118
153
  }
119
154
 
120
155
  /**
@@ -139,7 +174,7 @@ export interface KeyCascadeDeps {
139
174
  sourceLabel?: string;
140
175
  }
141
176
 
142
- /** The provider auth precedence cascade (runtime → config → OAuth → login key → env → stored key). */
177
+ /** The provider auth precedence cascade (runtime → config → OAuth → login key → env → stored key → compat mirror). */
143
178
  export class KeyCascade implements KeysApi {
144
179
  #deps: KeyCascadeDeps;
145
180
 
@@ -196,8 +231,8 @@ export class KeyCascade implements KeysApi {
196
231
  /**
197
232
  * Classify where a provider's auth comes from, following the same precedence
198
233
  * as {@link KeyCascade.get}: runtime override → config override →
199
- * stored OAuth → login-stored api_key → config fallback → env var → stored api_key.
200
- * Returns undefined when no auth is configured.
234
+ * stored OAuth → login-stored api_key → config fallback → env var →
235
+ * stored api_key → compat mirror. Returns undefined when no auth is configured.
201
236
  *
202
237
  * Compact, structured counterpart to {@link KeyCascade.describe}; `env`
203
238
  * selects dedicated-only, alias-aware, or no environment fallback.
@@ -219,6 +254,7 @@ export class KeyCascade implements KeysApi {
219
254
  return { kind: "env", envVar: getEnvApiKeyName(provider), concrete };
220
255
  }
221
256
  if (bearing.some(credential => credential.type === "api_key")) return { kind: "api_key", concrete: true };
257
+ if (this.#deps.overrides.mirrorKey(provider) !== undefined) return { kind: "config", concrete: true };
222
258
  return undefined;
223
259
  }
224
260
 
@@ -294,10 +330,18 @@ export class KeyCascade implements KeysApi {
294
330
  if (apiKeySelection) {
295
331
  return this.#deps.overrides.resolve(apiKeySelection.credential.key);
296
332
  }
333
+ const mirrorKey = this.#deps.overrides.mirrorKey(provider);
334
+ if (mirrorKey !== undefined) return this.#deps.overrides.resolve(mirrorKey);
297
335
  return undefined;
298
336
  }
299
337
 
300
- /** Resolve a bearer together with the stored row that supplied it. */
338
+ /**
339
+ * Resolve a bearer together with the stored row that supplied it, for a request.
340
+ * Same precedence as {@link KeyCascade.get}, but rejects with
341
+ * {@link AIError.OAuthRefreshUnavailableError} (transient, retryable) when OAuth
342
+ * refresh failed with a retryable error and no other permitted source could serve
343
+ * the request, so request paths retry instead of reporting a missing key.
344
+ */
301
345
  async getWithCredential(
302
346
  provider: string,
303
347
  sessionId?: string,
@@ -305,7 +349,7 @@ export class KeyCascade implements KeysApi {
305
349
  ): Promise<ResolvedApiKey | undefined> {
306
350
  let credentialId: number | undefined;
307
351
  let oauthIdentity: OAuthRequestIdentity | undefined;
308
- const apiKey = await this.get(provider, sessionId, options, (id, identity) => {
352
+ const apiKey = await this.#resolve(provider, sessionId, options, (id, identity) => {
309
353
  credentialId = id;
310
354
  oauthIdentity = identity;
311
355
  });
@@ -315,16 +359,36 @@ export class KeyCascade implements KeysApi {
315
359
  /**
316
360
  * Get API key for a provider.
317
361
  * Priority (first match wins): runtime override, config override, OAuth,
362
+ * login API key, environment variable, another stored API key, then the
363
+ * lowest-tier OMP compat mirror.
318
364
  * login API key, environment variable, then another stored API key.
365
+ * A session restricted by `sessions.restrict` resolves only its allowed
366
+ * OAuth accounts and throws {@link AIError.MissingApiKeyError} when none
367
+ * can serve or a config key (a models.yml `apiKey`, often for a proxy
368
+ * `baseUrl`) owns the provider, rather than using any other credential.
369
+ * A transient OAuth refresh failure resolves `undefined`, so availability
370
+ * probes move on to their next candidate; request paths use
371
+ * {@link KeyCascade.getWithCredential}, which surfaces it.
319
372
  */
320
- async get(
373
+ async get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
374
+ try {
375
+ return await this.#resolve(provider, sessionId, options);
376
+ } catch (error) {
377
+ if (!(error instanceof AIError.OAuthRefreshUnavailableError)) throw error;
378
+ logger.debug("API key probe skipped transient OAuth refresh failure", { provider, error: error.message });
379
+ return undefined;
380
+ }
381
+ }
382
+
383
+ async #resolve(
321
384
  provider: string,
322
385
  sessionId?: string,
323
386
  options?: AuthApiKeyOptions,
324
387
  onCredentialId?: (id: number, identity?: OAuthRequestIdentity) => void,
325
388
  ): Promise<string | undefined> {
389
+ const restricted = this.#deps.affinity.isRestricted(provider, sessionId);
326
390
  // Runtime override takes highest priority
327
- const runtimeKey = this.#deps.overrides.runtimeKey(provider);
391
+ const runtimeKey = restricted ? undefined : this.#deps.overrides.runtimeKey(provider);
328
392
  if (runtimeKey) {
329
393
  return runtimeKey;
330
394
  }
@@ -334,14 +398,29 @@ export class KeyCascade implements KeysApi {
334
398
  // (e.g. an auth-gateway) and supplied the bearer for that endpoint —
335
399
  // honor it instead of forwarding an upstream OAuth token that the proxy
336
400
  // won't accept.
337
- const configKey = this.#deps.overrides.configKey(provider);
401
+ if (restricted && this.#deps.overrides.configKey(provider) !== undefined) {
402
+ throw new AIError.MissingApiKeyError(
403
+ provider,
404
+ `No API key for provider: ${provider} (session ${sessionId} is restricted to its OAuth account pool, but models.yml configures an apiKey for this provider, and pooled OAuth tokens are never sent past it)`,
405
+ );
406
+ }
407
+ const configKey = restricted ? undefined : this.#deps.overrides.configKey(provider);
338
408
  if (configKey !== undefined) {
339
409
  return this.#deps.overrides.resolve(configKey);
340
410
  }
341
411
 
342
412
  // Precedence: a deliberate OAuth/login credential wins, then an explicit env var,
343
413
  // then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
344
- const oauthResolved = await this.#deps.selector.resolveOAuth(provider, sessionId, options);
414
+ // A transient OAuth refresh failure still lets later sources serve the request; it is
415
+ // rethrown only when none can, so callers retry instead of reporting a missing key.
416
+ let oauthResolved: OAuthResolutionResult | undefined;
417
+ let oauthRefreshFailure: AIError.OAuthRefreshUnavailableError | undefined;
418
+ try {
419
+ oauthResolved = await this.#deps.selector.resolveOAuth(provider, sessionId, options);
420
+ } catch (error) {
421
+ if (!(error instanceof AIError.OAuthRefreshUnavailableError)) throw error;
422
+ oauthRefreshFailure = error;
423
+ }
345
424
  if (oauthResolved) {
346
425
  if (onCredentialId && oauthResolved.credentialId !== undefined) {
347
426
  const { orgId, region, inferenceRegion } = oauthResolved.credential;
@@ -349,6 +428,13 @@ export class KeyCascade implements KeysApi {
349
428
  }
350
429
  return oauthResolved.apiKey;
351
430
  }
431
+ if (restricted) {
432
+ if (oauthRefreshFailure) throw oauthRefreshFailure;
433
+ throw new AIError.MissingApiKeyError(
434
+ provider,
435
+ `No API key for provider: ${provider} (session ${sessionId} is restricted to its OAuth account pool and none of those accounts is available)`,
436
+ );
437
+ }
352
438
  const loginApiKeySelection = await this.#deps.selector.selectApiKey(
353
439
  provider,
354
440
  sessionId,
@@ -356,7 +442,9 @@ export class KeyCascade implements KeysApi {
356
442
  credential => credential.source === "login" && !this.isKeylessFallback(provider, credential),
357
443
  );
358
444
  if (loginApiKeySelection) {
359
- this.#deps.affinity.record(provider, sessionId, "api_key", loginApiKeySelection.index);
445
+ if (options?.recordAffinity !== false) {
446
+ this.#deps.affinity.record(provider, sessionId, "api_key", loginApiKeySelection.index);
447
+ }
360
448
  const credentialId = onCredentialId
361
449
  ? this.#deps.pool.entries(provider)[loginApiKeySelection.index]?.id
362
450
  : undefined;
@@ -382,12 +470,17 @@ export class KeyCascade implements KeysApi {
382
470
  credential => credential.source !== "login",
383
471
  );
384
472
  if (apiKeySelection) {
385
- this.#deps.affinity.record(provider, sessionId, "api_key", apiKeySelection.index);
473
+ if (options?.recordAffinity !== false) {
474
+ this.#deps.affinity.record(provider, sessionId, "api_key", apiKeySelection.index);
475
+ }
386
476
  const credentialId = onCredentialId ? this.#deps.pool.entries(provider)[apiKeySelection.index]?.id : undefined;
387
477
  const apiKey = await this.#deps.overrides.resolve(apiKeySelection.credential.key);
388
478
  if (apiKey !== undefined && credentialId !== undefined) onCredentialId?.(credentialId);
389
479
  return apiKey;
390
480
  }
481
+ const mirrorKey = this.#deps.overrides.mirrorKey(provider);
482
+ if (mirrorKey !== undefined) return this.#deps.overrides.resolve(mirrorKey);
483
+ if (oauthRefreshFailure) throw oauthRefreshFailure;
391
484
  return undefined;
392
485
  }
393
486
 
@@ -454,6 +547,7 @@ export class KeyCascade implements KeysApi {
454
547
  * 4. API key persisted by a successful `/login`.
455
548
  * 5. Env var — overrides a stored static api_key (e.g. a stale broker copy).
456
549
  * 6. Stored api_key credential.
550
+ * 7. OMP compatibility mirror (upstream agent.db fallback key).
457
551
  *
458
552
  * The string is purely informational; consumers must not parse it.
459
553
  */
@@ -504,6 +598,7 @@ export class KeyCascade implements KeysApi {
504
598
  credential => credential.type !== "api_key" || credential.source !== "login",
505
599
  );
506
600
  if (apiKeySource) return apiKeySource;
601
+ if (this.#deps.overrides.mirrorKey(provider) !== undefined) return "OMP compatibility mirror (upstream agent.db)";
507
602
  return undefined;
508
603
  }
509
604
 
@@ -515,7 +610,7 @@ export class KeyCascade implements KeysApi {
515
610
  this.#deps.overrides.removeRuntime(provider);
516
611
  }
517
612
 
518
- setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void {
613
+ setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean; mirror?: boolean }): void {
519
614
  this.#deps.overrides.setConfig(provider, apiKeyConfig, options);
520
615
  }
521
616
 
@@ -1,5 +1,5 @@
1
1
  import type { Provider } from "../types";
2
- import { resolveUsedFraction } from "../usage";
2
+ import { isWithinUsageReserve, resolveUsedFraction } from "../usage";
3
3
  import type {
4
4
  CredentialRankingContext,
5
5
  UsageCredential,
@@ -224,7 +224,7 @@ export class CredentialHealth implements HealthApi {
224
224
  return {
225
225
  credentialId: entry.id,
226
226
  credentialType,
227
- state: remainingFraction <= resolveReserveFraction(entry) ? "reserve" : "healthy",
227
+ state: isWithinUsageReserve(remainingFraction, resolveReserveFraction(entry)) ? "reserve" : "healthy",
228
228
  remainingFraction,
229
229
  };
230
230
  }),
@@ -283,6 +283,7 @@ export class CredentialHealth implements HealthApi {
283
283
  const results: CredentialHealthResult[] = [];
284
284
  for (const row of stored) {
285
285
  options?.signal?.throwIfAborted();
286
+ if (options?.excludeProviders?.has(row.provider)) continue;
286
287
  const base: CredentialHealthResult = {
287
288
  id: row.id,
288
289
  provider: row.provider,
package/src/auth/oauth.ts CHANGED
@@ -127,15 +127,18 @@ export class OAuthAccounts implements OAuthApi {
127
127
  * `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
128
128
  * scenarios, prefer API-key resolution.
129
129
  *
130
- * Returns `undefined` when no OAuth credential is available, the
131
- * credential fails to refresh, or runtime/config overrides have replaced
132
- * OAuth with an explicit API key.
130
+ * Returns `undefined` when no usable OAuth credential is available
131
+ * (none stored, or every one definitively failed to refresh) or
132
+ * runtime/config overrides have replaced OAuth with an explicit API key.
133
+ * Rejects with {@link AIError.OAuthRefreshUnavailableError} (transient,
134
+ * retryable) when a retryable refresh failure left no usable credential.
133
135
  */
134
136
  async access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined> {
135
137
  // Runtime / config overrides intentionally short-circuit OAuth: when the
136
138
  // user has pinned an API key, they expect the OAuth identity to be
137
- // suppressed (same contract as account identity lookup).
138
- if (this.#deps.overrides.has(provider)) {
139
+ // suppressed (same contract as account identity lookup). A session
140
+ // restricted to an account pool never uses a runtime key.
141
+ if (this.#deps.overrides.suppressesOAuth(provider, this.#deps.affinity.isRestricted(provider, sessionId))) {
139
142
  return undefined;
140
143
  }
141
144
  const resolved = await this.#deps.selector.resolveOAuth(provider, sessionId, options);
@@ -176,9 +179,20 @@ export class OAuthAccounts implements OAuthApi {
176
179
  options: AuthApiKeyOptions | undefined,
177
180
  ): Promise<OAuthAccessResolution> {
178
181
  try {
182
+ // tryOAuth refreshes only expired tokens; it resyncs this row from the store after a forced re-mint.
183
+ let index = selection.index;
184
+ if (options?.forceRefresh) {
185
+ await this.refresh(selection.credentialId, options.signal, {
186
+ reuseRecentMint: options.refreshReason === "auth-recovery",
187
+ reason: options.refreshReason,
188
+ });
189
+ // The refresh re-lists rows, so a concurrently removed row can shift this one's position.
190
+ index = this.#deps.pool.entries(provider).findIndex(entry => entry.id === selection.credentialId);
191
+ if (index === -1) throw new Error(`OAuth credential ${selection.credentialId} was removed during refresh`);
192
+ }
179
193
  const resolved = await this.#deps.selector.tryOAuth(
180
194
  provider,
181
- { credential: selection.credential, index: selection.index },
195
+ { credential: selection.credential, index },
182
196
  providerKey,
183
197
  undefined,
184
198
  options,
@@ -236,7 +250,7 @@ export class OAuthAccounts implements OAuthApi {
236
250
  * credential.
237
251
  */
238
252
  accounts(provider: string, sessionId?: string): OAuthAccountSummary[] {
239
- if (this.#deps.overrides.has(provider)) {
253
+ if (this.#deps.overrides.suppressesOAuth(provider, this.#deps.affinity.isRestricted(provider, sessionId))) {
240
254
  return [];
241
255
  }
242
256
  const sessionCredential = this.#deps.affinity.get(provider, sessionId);
@@ -286,9 +300,10 @@ export class OAuthAccounts implements OAuthApi {
286
300
  * Resolve one stored OAuth credential by its durable storage row id.
287
301
  *
288
302
  * Unlike the normal session resolver, this method never ranks, rotates, or
289
- * falls back to sibling credentials. A forced refresh re-mints only the
290
- * requested row, preserving exact-account affinity for operations whose
291
- * provenance and policy boundary are tied to one workspace.
303
+ * falls back to sibling credentials. A generic forced refresh re-mints only the
304
+ * requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
305
+ * still-usable recent mint of that row. Either way exact-account affinity holds
306
+ * for operations whose provenance and policy boundary are tied to one workspace.
292
307
  *
293
308
  * Returns `undefined` when the row does not exist for `provider` or an
294
309
  * explicit runtime/config API-key override suppresses OAuth.
@@ -1,3 +1,4 @@
1
+ import { logger } from "@linxiraos/pi-utils";
1
2
  import * as AIError from "../error";
2
3
  import type {
3
4
  AuthAccountPolicies,
@@ -23,6 +24,8 @@ export function matchesAuthAccountSelector(selector: AuthAccountSelector, identi
23
24
  export class AccountPolicies {
24
25
  #accountPolicies: AuthAccountPolicies;
25
26
  #defaultReservePct: number;
27
+ /** Indexes of policies already warned as unmatched; validation runs on every selection. */
28
+ #warnedUnmatched = new Set<number>();
26
29
 
27
30
  constructor(policies: AuthAccountPolicies, defaultReservePct: number | undefined) {
28
31
  AccountPolicies.#validateAccountPolicyConfiguration(policies);
@@ -52,6 +55,7 @@ export class AccountPolicies {
52
55
  for (const [provider, credentials] of storedCredentials) next.validateFor(provider, credentials);
53
56
  this.#accountPolicies = next.#accountPolicies;
54
57
  this.#defaultReservePct = next.#defaultReservePct;
58
+ this.#warnedUnmatched = next.#warnedUnmatched;
55
59
  }
56
60
 
57
61
  static #validateAccountPolicyConfiguration(accountPolicies: AuthAccountPolicies): void {
@@ -105,6 +109,7 @@ export class AccountPolicies {
105
109
  }
106
110
  }
107
111
 
112
+ /** Throw on ambiguous selectors for `provider`; a selector whose account is absent is inert and warns once. */
108
113
  validateFor(provider: string, credentials: readonly AuthCredential[]): void {
109
114
  const policies = this.#accountPolicies
110
115
  .map((policy, index) => ({ policy, index }))
@@ -125,7 +130,11 @@ export class AccountPolicies {
125
130
  }
126
131
  const path = `auth.accountPolicies[${index}].account`;
127
132
  if (matches.length === 0) {
128
- throw new AIError.ConfigurationError(`${path} matches no stored OAuth account for ${provider}`);
133
+ if (!this.#warnedUnmatched.has(index)) {
134
+ this.#warnedUnmatched.add(index);
135
+ logger.warn("Account policy matches no stored OAuth account; ignoring it", { provider, path });
136
+ }
137
+ continue;
129
138
  }
130
139
  if (matches.length > 1) {
131
140
  throw new AIError.ConfigurationError(
package/src/auth/pool.ts CHANGED
@@ -642,11 +642,6 @@ export class CredentialPool implements CredentialsApi {
642
642
  const index = entries.findIndex(entry => entry.id === credentialId);
643
643
  if (index === -1) return false;
644
644
  const remainingEntries = entries.filter((_entry, entryIndex) => entryIndex !== index);
645
- this.#options.policies.validateFor(
646
- provider,
647
- remainingEntries.map(entry => entry.credential),
648
- );
649
-
650
645
  const deleted = await this.#store.deleteAuthCredential(credentialId, "deleted by user");
651
646
  if (!deleted) return false;
652
647
  this.replace(provider, remainingEntries);