@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
@@ -9,7 +9,7 @@ import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
10
  import { authPolicyFor } from "@linxiraos/pi-catalog/compat/auth";
11
11
  import { parseAlibabaTokenPlanCredential } from "@linxiraos/pi-catalog/wire/alibaba-token-plan";
12
- import { parseCloudflareAiGatewayCredential } from "@linxiraos/pi-catalog/wire/cloudflare-ai-gateway";
12
+ import { parseCloudflareCredential } from "@linxiraos/pi-catalog/wire/cloudflare";
13
13
  import {
14
14
  getAgentDbPath,
15
15
  getDbBusyTimeoutMs,
@@ -110,6 +110,12 @@ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
110
110
  const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
111
111
  const LEGACY_CODEX_BLOCK_SCOPE = "shared";
112
112
  const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
113
+ /**
114
+ * Minimum spacing between read-path sweeps of expired block rows. Reads already
115
+ * filter `blocked_until_ms > now`; the sweep only bounds table growth, so it
116
+ * must not take the SQLite write lock on every selection.
117
+ */
118
+ const CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS = 60_000;
113
119
 
114
120
  // SQLite error classifiers live in pi-utils so the credential store and the
115
121
  // model cache share one implementation; re-exported here to preserve the
@@ -211,6 +217,12 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
211
217
  return null;
212
218
  }
213
219
 
220
+ /**
221
+ * Identity key of a credential: the `identityKey` that broker snapshots carry
222
+ * and that broker account pools and `sessions.restrict` match, such as
223
+ * `email:<address>|org:<id>` for org-scoped providers. `null` for an API key or
224
+ * an OAuth credential without any account, email, project, or org identity.
225
+ */
214
226
  export function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null {
215
227
  if (credential.type === "api_key") return null;
216
228
  return resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential));
@@ -223,7 +235,11 @@ function resolveRowCredentialIdentityKey(provider: string, row: AuthRow): string
223
235
  return credential?.type === "oauth" ? resolveCredentialIdentityKey(provider, credential) : null;
224
236
  }
225
237
 
226
- function matchesReplacementCredential(
238
+ /**
239
+ * Whether upserting `incoming` replaces the active row holding `existing` (stored
240
+ * under `existingIdentityKey`) instead of inserting a new row beside it.
241
+ */
242
+ export function matchesReplacementCredential(
227
243
  provider: string,
228
244
  existing: AuthCredential | null,
229
245
  existingIdentityKey: string | null,
@@ -238,9 +254,9 @@ function matchesReplacementCredential(
238
254
  const incomingToken = parseAlibabaTokenPlanCredential(incoming.key)?.token;
239
255
  return existingToken !== undefined && existingToken === incomingToken;
240
256
  }
241
- if (provider === "cloudflare-ai-gateway") {
242
- const existingToken = parseCloudflareAiGatewayCredential(existing.key)?.token;
243
- const incomingToken = parseCloudflareAiGatewayCredential(incoming.key)?.token;
257
+ if (provider === "cloudflare-ai-gateway" || provider === "cloudflare-workers-ai") {
258
+ const existingToken = parseCloudflareCredential(existing.key)?.token;
259
+ const incomingToken = parseCloudflareCredential(incoming.key)?.token;
244
260
  return existingToken !== undefined && existingToken === incomingToken;
245
261
  }
246
262
  return false;
@@ -294,6 +310,23 @@ function matchesReplacementCredential(
294
310
  return false;
295
311
  }
296
312
 
313
+ /**
314
+ * Whether two OAuth credentials belong to the same account of the same organization,
315
+ * whichever identity key each is stored under. The email decides when both carry one;
316
+ * otherwise a matching account id does, unless it is the organization's own id (a
317
+ * ChatGPT workspace id shared by every member). Project ids are shared between users
318
+ * (every consumer Antigravity login has the same one) and never count.
319
+ */
320
+ export function isSameOAuthAccount(a: OAuthCredential, b: OAuthCredential): boolean {
321
+ const orgId = normalizeStoredAccountId(a.orgId);
322
+ if (orgId !== normalizeStoredAccountId(b.orgId)) return false;
323
+ const emailA = normalizeStoredEmail(a.email);
324
+ const emailB = normalizeStoredEmail(b.email);
325
+ if (emailA !== null && emailB !== null) return emailA === emailB;
326
+ const accountId = normalizeStoredAccountId(a.accountId);
327
+ return accountId !== null && accountId !== orgId && accountId === normalizeStoredAccountId(b.accountId);
328
+ }
329
+
297
330
  function extractOAuthCredentialIdentifiers(credential: OAuthCredential): string[] {
298
331
  const identifiers = new Set<string>();
299
332
  const accountId = normalizeStoredAccountId(credential.accountId);
@@ -386,11 +419,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
386
419
  #updateIfMatchesWithLeaseStmt: Statement;
387
420
  #deleteIfMatchesWithLeaseStmt: Statement;
388
421
  #getCredentialBlockStmt: Statement;
389
- #listCredentialBlocksByCredentialStmt: Statement;
422
+ #listCredentialBlockScopesStmt: Statement;
423
+ #listCredentialBlocksStmt: Statement;
390
424
  #upsertCredentialBlockStmt: Statement;
391
425
  #deleteCredentialBlocksStmt: Statement;
392
426
  #deleteCredentialBlockStmt: Statement;
393
427
  #deleteExpiredCredentialBlocksStmt: Statement;
428
+ /** Wall-clock time of the last expired-block sweep; throttles read-path sweeps. */
429
+ #lastCredentialBlockSweepMs = Number.NEGATIVE_INFINITY;
394
430
  #acquireCredentialRefreshLeaseStmt: Statement;
395
431
  #getCredentialRefreshLeaseStmt: Statement;
396
432
  #renewCredentialRefreshLeaseStmt: Statement;
@@ -475,12 +511,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
475
511
  this.#getCredentialBlockStmt = this.#db.prepare(
476
512
  "SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
477
513
  );
478
- this.#listCredentialBlocksByCredentialStmt = this.#db.prepare(
479
- `SELECT credential_id, provider_key, block_scope, blocked_until_ms, updated_at
480
- FROM auth_credential_blocks
481
- WHERE credential_id = ? AND blocked_until_ms > ?
482
- AND NOT (provider_key = ? AND block_scope = ?)
483
- ORDER BY provider_key ASC, block_scope ASC`,
514
+ this.#listCredentialBlockScopesStmt = this.#db.prepare(
515
+ "SELECT block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND blocked_until_ms > ?",
516
+ );
517
+ // One statement for any number of ids: `json_each` keeps the caller's id
518
+ // order (`ids.key`) and the per-credential provider/scope order.
519
+ this.#listCredentialBlocksStmt = this.#db.prepare(
520
+ `SELECT b.credential_id, b.provider_key, b.block_scope, b.blocked_until_ms, b.updated_at
521
+ FROM json_each(?) AS ids
522
+ JOIN auth_credential_blocks AS b ON b.credential_id = ids.value
523
+ WHERE b.blocked_until_ms > ?
524
+ AND NOT (b.provider_key = ? AND b.block_scope = ?)
525
+ ORDER BY ids.key ASC, b.provider_key ASC, b.block_scope ASC`,
484
526
  );
485
527
  this.#upsertCredentialBlockStmt = this.#db.prepare(
486
528
  `INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
@@ -576,7 +618,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
576
618
  * Install the per-connection busy handler so lock-taking statements wait for
577
619
  * a contended writer instead of failing immediately (Bun defaults
578
620
  * `busy_timeout` to 0). MUST run before the first lock-taking statement on
579
- * the connection: concurrent zeta startups race WAL recovery and the leases
621
+ * the connection: concurrent omp startups race WAL recovery and the leases
580
622
  * DDL. Uses the centralized timeout so headless hosts keep their bounded
581
623
  * busy wait instead of the interactive 5s value. See issues #2421, #7298.
582
624
  */
@@ -587,7 +629,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
587
629
  #initializeSchema(): void {
588
630
  // Install the busy handler BEFORE any lock-taking statement (incl.
589
631
  // `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
590
- // recovery). Without this, concurrent zeta startups can crash here with
632
+ // recovery). Without this, concurrent omp startups can crash here with
591
633
  // `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. Re-setting when opened via
592
634
  // `open()` (which already installed it) is idempotent. See issue #2421.
593
635
  SqliteAuthCredentialStore.#installBusyTimeout(this.#db);
@@ -1585,27 +1627,38 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1585
1627
  }
1586
1628
 
1587
1629
  getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
1588
- const nowMs = Date.now();
1589
- const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
1590
1630
  // Current callers use meter scopes. The physical shared row exists only
1591
1631
  // for direct SQLite readers from pre-meter releases.
1592
- if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1632
+ if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1593
1633
  return undefined;
1594
1634
  }
1595
- if (!isCodexBlock) this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1596
- const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
1635
+ this.#sweepExpiredCredentialBlocks();
1636
+ const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, Date.now()) as
1597
1637
  | { blocked_until_ms?: number; updated_at?: number }
1598
1638
  | undefined;
1599
1639
  return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
1600
1640
  }
1601
1641
 
1642
+ getCredentialBlockScopes(credentialId: number, providerKey: string): Map<string, number> {
1643
+ this.#sweepExpiredCredentialBlocks();
1644
+ const rows = this.#listCredentialBlockScopesStmt.all(credentialId, providerKey, Date.now()) as Array<
1645
+ Pick<CredentialBlockRow, "block_scope" | "blocked_until_ms">
1646
+ >;
1647
+ const hidesLegacyShared = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
1648
+ const scopes = new Map<string, number>();
1649
+ for (const row of rows) {
1650
+ if (hidesLegacyShared && row.block_scope === LEGACY_CODEX_BLOCK_SCOPE) continue;
1651
+ scopes.set(row.block_scope, row.blocked_until_ms);
1652
+ }
1653
+ return scopes;
1654
+ }
1655
+
1602
1656
  getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
1603
- const nowMs = Date.now();
1604
- const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
1605
- if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1657
+ if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1606
1658
  return undefined;
1607
1659
  }
1608
- if (!isCodexBlock) this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1660
+ this.#sweepExpiredCredentialBlocks();
1661
+ const nowMs = Date.now();
1609
1662
  const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
1610
1663
  | { blocked_until_ms?: number; updated_at?: number }
1611
1664
  | undefined;
@@ -1622,7 +1675,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1622
1675
  const isLegacyCodexBlock =
1623
1676
  block.providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && block.blockScope === LEGACY_CODEX_BLOCK_SCOPE;
1624
1677
  const blockScopes = isLegacyCodexBlock ? CODEX_METER_BLOCK_SCOPES : [block.blockScope];
1678
+ const nowMs = Date.now();
1625
1679
  const upsert = this.#db.transaction(() => {
1680
+ // A mark already holds the write lock: sweep expired rows in the same transaction.
1681
+ this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1626
1682
  for (const blockScope of blockScopes) {
1627
1683
  this.#upsertCredentialBlockStmt.run(
1628
1684
  block.credentialId,
@@ -1633,8 +1689,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1633
1689
  }
1634
1690
  });
1635
1691
  upsert.immediate();
1692
+ this.#lastCredentialBlockSweepMs = nowMs;
1693
+ this.#pruneCredentialBlockReconcileAfter(nowMs);
1636
1694
 
1637
- const reconcileAfterMs = Math.min(block.blockedUntilMs, Date.now() + USAGE_REPORT_TTL_MS);
1695
+ const reconcileAfterMs = Math.min(block.blockedUntilMs, nowMs + USAGE_REPORT_TTL_MS);
1638
1696
  for (const blockScope of blockScopes) {
1639
1697
  this.#credentialBlockReconcileAfter.set(
1640
1698
  `${block.credentialId}\0${block.providerKey}\0${blockScope}`,
@@ -1661,38 +1719,49 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1661
1719
  }
1662
1720
 
1663
1721
  cleanExpiredCredentialBlocks(nowMs: number): void {
1722
+ // Stamp wall-clock time first: callers may pass a future `nowMs`, and a
1723
+ // failed sweep must not be retried by every following read.
1724
+ this.#lastCredentialBlockSweepMs = Date.now();
1664
1725
  this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1726
+ this.#pruneCredentialBlockReconcileAfter(nowMs);
1727
+ }
1728
+
1729
+ #pruneCredentialBlockReconcileAfter(nowMs: number): void {
1665
1730
  for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
1666
1731
  if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
1667
1732
  }
1668
1733
  }
1669
1734
 
1670
- listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
1671
- if (credentialIds.length === 0) return [];
1735
+ /**
1736
+ * Read-path expiry sweep, at most once per {@link CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS}.
1737
+ * A busy write lock only postpones housekeeping; it never fails the read.
1738
+ */
1739
+ #sweepExpiredCredentialBlocks(): void {
1672
1740
  const nowMs = Date.now();
1673
- this.cleanExpiredCredentialBlocks(nowMs);
1674
- const seenCredentialIds = new Set<number>();
1675
- const blocks: StoredCredentialBlock[] = [];
1676
- for (const credentialId of credentialIds) {
1677
- if (seenCredentialIds.has(credentialId)) continue;
1678
- seenCredentialIds.add(credentialId);
1679
- const rows = this.#listCredentialBlocksByCredentialStmt.all(
1680
- credentialId,
1681
- nowMs,
1682
- LEGACY_CODEX_BLOCK_PROVIDER_KEY,
1683
- LEGACY_CODEX_BLOCK_SCOPE,
1684
- ) as CredentialBlockRow[];
1685
- for (const row of rows) {
1686
- blocks.push({
1687
- credentialId: row.credential_id,
1688
- providerKey: row.provider_key,
1689
- blockScope: row.block_scope,
1690
- blockedUntilMs: row.blocked_until_ms,
1691
- updatedAtMs: row.updated_at * 1000,
1692
- });
1693
- }
1741
+ if (nowMs - this.#lastCredentialBlockSweepMs < CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS) return;
1742
+ try {
1743
+ this.cleanExpiredCredentialBlocks(nowMs);
1744
+ } catch (err) {
1745
+ if (!isSqliteBusyError(err)) throw err;
1694
1746
  }
1695
- return blocks;
1747
+ }
1748
+
1749
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
1750
+ if (credentialIds.length === 0) return [];
1751
+ this.#sweepExpiredCredentialBlocks();
1752
+ const rows = this.#listCredentialBlocksStmt.all(
1753
+ JSON.stringify([...new Set(credentialIds)]),
1754
+ Date.now(),
1755
+ LEGACY_CODEX_BLOCK_PROVIDER_KEY,
1756
+ LEGACY_CODEX_BLOCK_SCOPE,
1757
+ ) as CredentialBlockRow[];
1758
+ return rows.map(row => ({
1759
+ credentialId: row.credential_id,
1760
+ providerKey: row.provider_key,
1761
+ blockScope: row.block_scope,
1762
+ blockedUntilMs: row.blocked_until_ms,
1763
+ updatedAtMs: row.updated_at * 1000,
1764
+ }));
1696
1765
  }
1697
1766
 
1698
1767
  tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
@@ -2050,7 +2119,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
2050
2119
  this.#upsertCacheStmt.finalize();
2051
2120
  this.#deleteExpiredCacheStmt.finalize();
2052
2121
  this.#getCredentialBlockStmt.finalize();
2053
- this.#listCredentialBlocksByCredentialStmt.finalize();
2122
+ this.#listCredentialBlockScopesStmt.finalize();
2123
+ this.#listCredentialBlocksStmt.finalize();
2054
2124
  this.#upsertCredentialBlockStmt.finalize();
2055
2125
  this.#deleteCredentialBlocksStmt.finalize();
2056
2126
  this.#deleteCredentialBlockStmt.finalize();
package/src/auth/store.ts CHANGED
@@ -98,6 +98,12 @@ export interface CredentialCacheStore {
98
98
  export interface CredentialBlockStore {
99
99
  /** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
100
100
  getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined;
101
+ /**
102
+ * Non-expired blocks for one credential and providerKey, keyed by block
103
+ * scope (`""` is unscoped), in one read. Optional: selectors fall back to
104
+ * {@link CredentialBlockStore.getCredentialBlock} per scope when absent.
105
+ */
106
+ getCredentialBlockScopes?(credentialId: number, providerKey: string): ReadonlyMap<string, number>;
101
107
  /** Earliest time a shared-store block should be eligible for live-usage reconciliation. */
102
108
  getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined;
103
109
  /** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
@@ -146,8 +152,8 @@ export interface UsageLedgerStore {
146
152
 
147
153
  /** Broker-delegated OAuth refresh and usage-report operations. */
148
154
  export interface CredentialUpstream {
149
- /** Optional hook to notify the underlying store that usage report cache is stale. */
150
- invalidateUsageCache(signal?: AbortSignal): Promise<void>;
155
+ /** Optional hook to notify the underlying store that usage reports (one provider's, when given) are stale. */
156
+ invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
151
157
  /**
152
158
  * Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
153
159
  * it before the per-provider local refresh path. `RemoteAuthCredentialStore`
package/src/auth/types.ts CHANGED
@@ -257,6 +257,8 @@ export interface CheckCredentialsOptions {
257
257
  completionProbe?: CompletionProbe;
258
258
  /** Per-credential completion probe timeout (ms). Defaults to `timeoutMs`. */
259
259
  completionTimeoutMs?: number;
260
+ /** Providers whose credentials are skipped entirely (not probed, not reported). */
261
+ excludeProviders?: ReadonlySet<string>;
260
262
  }
261
263
 
262
264
  // ─────────────────────────────────────────────────────────────────────────────
@@ -371,6 +373,12 @@ export type AuthStorageOptions = {
371
373
  signal?: AbortSignal,
372
374
  reason?: OAuthRefreshReason,
373
375
  ) => Promise<OAuthCredentials>;
376
+ /**
377
+ * Set when {@link refreshOAuthCredential} exchanges the refresh token with the
378
+ * provider itself instead of delegating (e.g. to a broker), so its tokens count
379
+ * as this process's mints for auth-recovery reuse.
380
+ */
381
+ refreshOAuthCredentialMints?: boolean;
374
382
  /**
375
383
  * Human-readable description of the credential store backing this
376
384
  * AuthStorage instance. Surfaced through {@link AuthStorage.keys.describe}
@@ -470,7 +478,7 @@ export interface ModelUsageHealthOptions {
470
478
  export type AuthApiKeyOptions = {
471
479
  baseUrl?: string;
472
480
  modelId?: string;
473
- /** Provider account ids known to serve `modelId` from multi-account discovery; OAuth selection prefers them and tries other accounts only as a last resort. */
481
+ /** {@link oauthAccountKey}s of accounts known to serve `modelId` from multi-account discovery; OAuth selection prefers them and tries other accounts only as a last resort. */
474
482
  accountIds?: readonly string[];
475
483
  /**
476
484
  * Caller's cancel signal. Threaded into any broker-bound OAuth refresh so
@@ -487,8 +495,26 @@ export type AuthApiKeyOptions = {
487
495
  forceRefresh?: boolean;
488
496
  /** Explicit provider-401 recovery; generic force refreshes leave this unset. */
489
497
  refreshReason?: OAuthRefreshReason;
498
+ /** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
499
+ recordAffinity?: boolean;
490
500
  };
491
501
 
502
+ /**
503
+ * Identity that multi-account discovery records in `Model.accountAccess` and
504
+ * that {@link AuthApiKeyOptions.accountIds} selection matches: the provider
505
+ * account id (Codex `chatgpt_account_id`), else the login email for providers
506
+ * whose credentials carry no account id (Antigravity), else the project id for
507
+ * a login whose email lookup failed. Same precedence as the credential store's
508
+ * identity key.
509
+ */
510
+ export function oauthAccountKey(identity: {
511
+ accountId?: string;
512
+ email?: string;
513
+ projectId?: string;
514
+ }): string | undefined {
515
+ return identity.accountId ?? identity.email ?? identity.projectId;
516
+ }
517
+
492
518
  /** Non-secret identity bound to the OAuth credential selected for one request attempt. */
493
519
  export interface OAuthRequestIdentity {
494
520
  orgId?: string;
@@ -890,9 +916,22 @@ export interface KeysApi {
890
916
  * 4. API key persisted by a successful `/login`
891
917
  * 5. Environment variable
892
918
  * 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
919
+ * 7. OMP compatibility mirror — below every stored credential, so a key the
920
+ * user saves locally always wins over the upstream fallback
921
+ *
922
+ * Resolves `undefined` when no permitted credential can serve, including when
923
+ * OAuth refresh failed transiently (network, timeout, 5xx), so availability
924
+ * probes move on to their next candidate. Request paths use
925
+ * {@link KeysApi.getWithCredential}, which surfaces that failure.
893
926
  */
894
927
  get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
895
- /** Resolve a bearer together with its durable stored credential row id, when known. */
928
+ /**
929
+ * Resolve a bearer together with its durable stored credential row id, when known.
930
+ * Same precedence as {@link KeysApi.get}, but rejects with
931
+ * `OAuthRefreshUnavailableError` (transient, retryable) when OAuth refresh failed
932
+ * with a retryable error and no other permitted source could serve the request;
933
+ * the stored credential is kept.
934
+ */
896
935
  getWithCredential(
897
936
  provider: string,
898
937
  sessionId?: string,
@@ -951,10 +990,14 @@ export interface KeysApi {
951
990
  * `fallback: true` ranks the value below stored OAuth and `/login`
952
991
  * credentials instead, so a provider's default key reference cannot shadow
953
992
  * a key the user logged in with.
993
+ *
994
+ * `mirror: true` ranks the value below everything else, under stored
995
+ * api_key credentials — a read-only compatibility mirror (upstream OMP
996
+ * agent.db key) that must never shadow a locally stored key.
954
997
  */
955
- setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void;
998
+ setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean; mirror?: boolean }): void;
956
999
  /**
957
- * Remove a single config-sourced API key (override or fallback).
1000
+ * Remove a single config-sourced API key (override, fallback, or mirror).
958
1001
  */
959
1002
  removeConfig(provider: string): void;
960
1003
  /**
@@ -983,7 +1026,7 @@ export interface KeysApi {
983
1026
  resolver(provider: string, options?: { sessionId?: string; baseUrl?: string; modelId?: string }): ApiKeyResolver;
984
1027
  }
985
1028
 
986
- /** Controls whether a row-id refresh may reuse a token minted by this refresher. */
1029
+ /** Controls whether a row-id refresh may reuse a recently minted token. */
987
1030
  export interface OAuthRefreshByIdOptions {
988
1031
  /**
989
1032
  * Return the stored credential when it still holds a fresh access token this
@@ -991,6 +1034,12 @@ export interface OAuthRefreshByIdOptions {
991
1034
  * refresh tokens repeatedly when a provider rejects every valid bearer.
992
1035
  */
993
1036
  reuseRecentMint?: boolean;
1037
+ /**
1038
+ * Provider-401 recovery intent. Forwarded to a delegated refresh (an auth
1039
+ * broker), which may then return a token it minted in the last five minutes.
1040
+ * Local reuse still requires {@link reuseRecentMint}.
1041
+ */
1042
+ reason?: OAuthRefreshReason;
994
1043
  }
995
1044
 
996
1045
  /** OAuth login, access, account identity, and refresh operations. */
@@ -1013,9 +1062,11 @@ export interface OAuthApi {
1013
1062
  * `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
1014
1063
  * scenarios, prefer {@link AuthStorage.keys.get}.
1015
1064
  *
1016
- * Returns `undefined` when no OAuth credential is available, the
1017
- * credential fails to refresh, or runtime/config overrides have replaced
1018
- * OAuth with an explicit API key.
1065
+ * Returns `undefined` when no usable OAuth credential is available (none
1066
+ * stored, or every one definitively failed to refresh) or runtime/config
1067
+ * overrides have replaced OAuth with an explicit API key. Rejects with
1068
+ * `OAuthRefreshUnavailableError` (transient, retryable) when a retryable
1069
+ * refresh failure left no usable credential.
1019
1070
  */
1020
1071
  access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
1021
1072
  /**
@@ -1031,9 +1082,10 @@ export interface OAuthApi {
1031
1082
  * Resolve one stored OAuth credential by its durable storage row id.
1032
1083
  *
1033
1084
  * Unlike the normal session resolver, this method never ranks, rotates, or
1034
- * falls back to sibling credentials. A forced refresh re-mints only the
1035
- * requested row, preserving exact-account affinity for operations whose
1036
- * provenance and policy boundary are tied to one workspace.
1085
+ * falls back to sibling credentials. A generic forced refresh re-mints only the
1086
+ * requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
1087
+ * still-usable recent mint of that row. Either way exact-account affinity holds
1088
+ * for operations whose provenance and policy boundary are tied to one workspace.
1037
1089
  *
1038
1090
  * Returns `undefined` when the row does not exist for `provider` or an
1039
1091
  * explicit runtime/config API-key override suppresses OAuth.
@@ -1071,8 +1123,9 @@ export interface OAuthApi {
1071
1123
  * Refresh the OAuth credential with the given id through a per-credential
1072
1124
  * single-flight. Concurrent callers for the same row await the same upstream
1073
1125
  * refresh attempt, which is required for providers that rotate refresh tokens
1074
- * on every successful refresh. Mints unconditionally unless
1075
- * {@link OAuthRefreshByIdOptions.reuseRecentMint} is set.
1126
+ * on every successful refresh. Mints unless {@link OAuthRefreshByIdOptions.reuseRecentMint}
1127
+ * finds a recent local mint, or a delegated refresh given
1128
+ * {@link OAuthRefreshByIdOptions.reason} `"auth-recovery"` returns its recent mint.
1076
1129
  */
1077
1130
  refresh(id: number, signal?: AbortSignal, options?: OAuthRefreshByIdOptions): Promise<AuthCredentialSnapshotEntry>;
1078
1131
  /**
@@ -1084,6 +1137,12 @@ export interface OAuthApi {
1084
1137
  ): Promise<StoredOAuthRefreshResult<T>>;
1085
1138
  }
1086
1139
 
1140
+ /**
1141
+ * Handle for one {@link SessionsApi.restrict} call. {@link SessionsApi.unrestrict}
1142
+ * lifts a restriction only with the lease of the call that installed it.
1143
+ */
1144
+ export type SessionRestrictionLease = symbol;
1145
+
1087
1146
  /** Session credential affinity operations. */
1088
1147
  export interface SessionsApi {
1089
1148
  /**
@@ -1101,14 +1160,46 @@ export interface SessionsApi {
1101
1160
  */
1102
1161
  pin(provider: string, sessionId: string, credentialId: number, options?: { restoredAtMs?: number }): boolean;
1103
1162
  /**
1104
- * Copy every stored credential affinity from one live session to another.
1163
+ * Copy stored credential affinities from one live session to another.
1105
1164
  *
1106
1165
  * The target receives its own sticky entries, so request resolution, usage
1107
1166
  * blocking, credential rotation, metadata, and persisted pins all continue
1108
1167
  * through the target session id without retaining a live dependency on the
1109
- * source session.
1110
- */
1111
- inherit(sourceSessionId: string, targetSessionId: string): number;
1168
+ * source session. `include` limits the copy to the providers it accepts,
1169
+ * given whether the source's affinity is an explicit user pin.
1170
+ */
1171
+ inherit(
1172
+ sourceSessionId: string,
1173
+ targetSessionId: string,
1174
+ include?: (provider: string, explicit: boolean) => boolean,
1175
+ ): number;
1176
+ /**
1177
+ * Restrict one session's credentials for `provider` to the OAuth accounts
1178
+ * whose identity key (`email:<address>|org:<id>` for org-scoped providers;
1179
+ * otherwise `account:`, `email:`, or `project:` — the keys broker account
1180
+ * pools use) is listed. Selection, session pins, inherited affinity,
1181
+ * fallback passes, and rotation never leave the list, and runtime, config,
1182
+ * environment, and stored API keys are not used for that session; when no
1183
+ * listed account can serve, key resolution fails instead of borrowing
1184
+ * another account. An empty list allows no account. Replaces any earlier
1185
+ * restriction for the same provider and session; {@link inherit} does not
1186
+ * copy restrictions. Restrictions are never evicted, because that would
1187
+ * widen a live session; the owner passes the returned lease to
1188
+ * {@link unrestrict} when the session ends.
1189
+ */
1190
+ restrict(provider: string, sessionId: string, identityKeys: readonly string[]): SessionRestrictionLease;
1191
+ /**
1192
+ * Lift the restriction that `lease` installed. Once a later {@link restrict}
1193
+ * call has replaced it for the same provider and session, this does nothing,
1194
+ * so a stale owner never lifts the restriction a newer owner relies on.
1195
+ */
1196
+ unrestrict(provider: string, sessionId: string, lease: SessionRestrictionLease): void;
1197
+ /**
1198
+ * Whether stored credential `credentialId` may serve `sessionId`: always for
1199
+ * an unrestricted session, otherwise only an OAuth account its
1200
+ * {@link restrict} list names.
1201
+ */
1202
+ permits(provider: string, sessionId: string, credentialId: number): boolean;
1112
1203
  /**
1113
1204
  * Release a session's sticky credential so its next {@link getApiKey} call
1114
1205
  * re-runs native pool ranking. This never blocks or penalizes the released
@@ -6,6 +6,7 @@ import type { UsageCredential, UsageProvider, UsageReport } from "../usage";
6
6
 
7
7
  const USAGE_CACHE_PREFIX = "usage_cache:";
8
8
  const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
9
+ const USAGE_REPORT_KEY_PREFIX = "report:";
9
10
  /** Minimum interval between non-exhausted header snapshots; used by UsageService. */
10
11
  export const USAGE_HEADER_INGEST_INTERVAL_MS = 60_000;
11
12
  const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
@@ -45,6 +46,13 @@ function parseUsageCacheEntry<T>(raw: string): UsageCacheEntry<T> | undefined {
45
46
  }
46
47
  }
47
48
 
49
+ /** Cached reports drop the provider's upstream `raw` payload; only a fresh fetch carries it. */
50
+ function withoutRawPayload(value: unknown): unknown {
51
+ if (value === null || typeof value !== "object" || !("raw" in value)) return value;
52
+ const { raw: _raw, ...rest } = value;
53
+ return rest;
54
+ }
55
+
48
56
  /** Convert a stored credential for usage providers; used by usage and health services. */
49
57
  export function buildUsageCredential(credential: AuthCredential): UsageCredential {
50
58
  if (credential.type === "api_key") {
@@ -196,12 +204,13 @@ export class UsageCache {
196
204
  }
197
205
 
198
206
  set<T>(key: string, entry: UsageCacheEntry<T>): void {
207
+ const value = withoutRawPayload(entry.value);
199
208
  const payload = JSON.stringify({
200
- value: entry.value,
209
+ value,
201
210
  expiresAt: entry.expiresAt,
202
211
  });
203
212
  const durableExpiresAt =
204
- entry.value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
213
+ value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
205
214
  this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
206
215
  }
207
216
 
@@ -272,7 +281,7 @@ export class UsageCache {
272
281
  const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
273
282
  const identity = usageCacheIdentity(request.credential);
274
283
  const providerKey = this.#usageCacheProviderKey(request.provider);
275
- const cacheKey = `report:${providerKey}:${baseUrl}:${identity}`;
284
+ const cacheKey = `${USAGE_REPORT_KEY_PREFIX}${providerKey}:${baseUrl}:${identity}`;
276
285
  const cacheKeys = this.#usageReportCacheKeysByProvider.get(request.provider) ?? new Set<string>();
277
286
  cacheKeys.add(cacheKey);
278
287
  this.#usageReportCacheKeysByProvider.set(request.provider, cacheKeys);
@@ -334,7 +343,7 @@ export class UsageCache {
334
343
  this.#epoch += 1;
335
344
  this.#bumpRefreshEpoch(provider);
336
345
  const expired = Date.now() - 1;
337
- const prefix = `report:${this.#usageCacheProviderKey(provider)}:`;
346
+ const prefix = `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`;
338
347
  if (this.deletePrefix(prefix)) return;
339
348
  const cacheKeys = new Set(this.#usageReportCacheKeysByProvider.get(provider));
340
349
  for (const entry of this.#pool.entries(provider)) {
@@ -361,7 +370,9 @@ export class UsageCache {
361
370
  ): Promise<void> {
362
371
  this.#epoch += 1;
363
372
  this.#bumpRefreshEpoch(provider);
364
- const prefix = provider ? `report:${this.#usageCacheProviderKey(provider)}:` : "report:";
373
+ const prefix = provider
374
+ ? `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`
375
+ : USAGE_REPORT_KEY_PREFIX;
365
376
  if (!this.deletePrefix(prefix)) {
366
377
  // Third-party stores may not support prefix deletion. Clear every active
367
378
  // request key instead, including API-key and environment credentials.
package/src/auth/usage.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { authPolicyFor } from "@linxiraos/pi-catalog/compat/auth";
2
2
  import { $pickenv, logger } from "@linxiraos/pi-utils";
3
+ import { getEnvApiKey } from "../env-api-key";
3
4
  import * as AIError from "../error";
4
- import { getEnvApiKey } from "../stream";
5
5
  import type { OAuthCredentials } from "../registry/oauth/types";
6
6
  import type { Provider } from "../types";
7
7
  import { resolveUsedFraction } from "../usage";
@@ -871,7 +871,7 @@ export class UsageService implements UsageApi {
871
871
  await this.#deps.cache.clearReports(provider, () => this.#collectUsageRequests());
872
872
 
873
873
  if (this.#deps.store.invalidateUsageCache) {
874
- await this.#deps.store.invalidateUsageCache(signal).catch(err => {
874
+ await this.#deps.store.invalidateUsageCache(provider, signal).catch(err => {
875
875
  logger.debug("Failed to notify store of stale usage", { err });
876
876
  });
877
877
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * HTTP client for the omp auth-broker server.
2
+ * HTTP client for the zetacode auth-broker server.
3
3
  *
4
4
  * Used by {@link RemoteAuthCredentialStore} (snapshot pulls) and by
5
- * `omp auth-broker status` (liveness checks). All endpoints except
5
+ * `zetacode auth-broker status` (liveness checks). All endpoints except
6
6
  * `/v1/healthz` require a bearer token.
7
7
  */
8
8
 
@@ -309,8 +309,11 @@ export class AuthBrokerClient {
309
309
  });
310
310
  }
311
311
 
312
- notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse> {
313
- return this.#request<UsageStaleResponse>("POST", "/v1/usage/stale", {
312
+ notifyUsageStale(provider?: string, signal?: AbortSignal): Promise<UsageStaleResponse> {
313
+ const params = new URLSearchParams();
314
+ if (provider) params.set("provider", provider);
315
+ const path = `/v1/usage/stale${params.size > 0 ? `?${params.toString()}` : ""}`;
316
+ return this.#request<UsageStaleResponse>("POST", path, {
314
317
  schema: "usageStaleResponseSchema",
315
318
  signal,
316
319
  });