@linxiraos/pi-ai 1.0.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 (639) hide show
  1. package/CHANGELOG.md +5066 -0
  2. package/README.md +1195 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  5. package/dist/types/auth-broker/client.d.ts +93 -0
  6. package/dist/types/auth-broker/discover.d.ts +39 -0
  7. package/dist/types/auth-broker/index.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +136 -0
  10. package/dist/types/auth-broker/server.d.ts +48 -0
  11. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  12. package/dist/types/auth-broker/types.d.ts +152 -0
  13. package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
  14. package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
  15. package/dist/types/auth-gateway/http.d.ts +56 -0
  16. package/dist/types/auth-gateway/index.d.ts +3 -0
  17. package/dist/types/auth-gateway/server.d.ts +36 -0
  18. package/dist/types/auth-gateway/types.d.ts +127 -0
  19. package/dist/types/auth-retry.d.ts +150 -0
  20. package/dist/types/auth-storage.d.ts +1258 -0
  21. package/dist/types/dialect/anthropic.d.ts +15 -0
  22. package/dist/types/dialect/catalog.d.ts +3 -0
  23. package/dist/types/dialect/coercion.d.ts +23 -0
  24. package/dist/types/dialect/deepseek.d.ts +14 -0
  25. package/dist/types/dialect/demotion.d.ts +30 -0
  26. package/dist/types/dialect/examples.d.ts +18 -0
  27. package/dist/types/dialect/factory.d.ts +3 -0
  28. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  29. package/dist/types/dialect/gemini.d.ts +17 -0
  30. package/dist/types/dialect/gemma.d.ts +15 -0
  31. package/dist/types/dialect/glm.d.ts +9 -0
  32. package/dist/types/dialect/harmony.d.ts +8 -0
  33. package/dist/types/dialect/hermes.d.ts +9 -0
  34. package/dist/types/dialect/history.d.ts +3 -0
  35. package/dist/types/dialect/index.d.ts +11 -0
  36. package/dist/types/dialect/inventory.d.ts +9 -0
  37. package/dist/types/dialect/kimi.d.ts +14 -0
  38. package/dist/types/dialect/minimax.d.ts +3 -0
  39. package/dist/types/dialect/owned-stream.d.ts +4 -0
  40. package/dist/types/dialect/qwen3.d.ts +9 -0
  41. package/dist/types/dialect/rendering.d.ts +54 -0
  42. package/dist/types/dialect/thinking.d.ts +6 -0
  43. package/dist/types/dialect/types.d.ts +68 -0
  44. package/dist/types/dialect/xml.d.ts +9 -0
  45. package/dist/types/error/abort.d.ts +14 -0
  46. package/dist/types/error/auth-classify.d.ts +20 -0
  47. package/dist/types/error/auth.d.ts +27 -0
  48. package/dist/types/error/aws.d.ts +27 -0
  49. package/dist/types/error/classes.d.ts +106 -0
  50. package/dist/types/error/finalize.d.ts +39 -0
  51. package/dist/types/error/flags.d.ts +94 -0
  52. package/dist/types/error/format.d.ts +20 -0
  53. package/dist/types/error/gateway.d.ts +20 -0
  54. package/dist/types/error/index.d.ts +14 -0
  55. package/dist/types/error/oauth.d.ts +43 -0
  56. package/dist/types/error/provider.d.ts +42 -0
  57. package/dist/types/error/rate-limit.d.ts +80 -0
  58. package/dist/types/error/retryable.d.ts +27 -0
  59. package/dist/types/error/validation.d.ts +32 -0
  60. package/dist/types/index.d.ts +51 -0
  61. package/dist/types/provider-details.d.ts +24 -0
  62. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  63. package/dist/types/providers/anthropic-client.d.ts +106 -0
  64. package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
  65. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  66. package/dist/types/providers/anthropic-wire.d.ts +345 -0
  67. package/dist/types/providers/anthropic.d.ts +261 -0
  68. package/dist/types/providers/aws-credentials.d.ts +48 -0
  69. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  70. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  71. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  72. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  73. package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
  74. package/dist/types/providers/cowork-fetch.d.ts +3 -0
  75. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  76. package/dist/types/providers/cursor-pi-args.d.ts +105 -0
  77. package/dist/types/providers/cursor.d.ts +221 -0
  78. package/dist/types/providers/devin.d.ts +12 -0
  79. package/dist/types/providers/error-message.d.ts +25 -0
  80. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  81. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  82. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  83. package/dist/types/providers/google-auth.d.ts +24 -0
  84. package/dist/types/providers/google-gemini-cli.d.ts +120 -0
  85. package/dist/types/providers/google-shared.d.ts +203 -0
  86. package/dist/types/providers/google-types.d.ts +155 -0
  87. package/dist/types/providers/google-vertex.d.ts +7 -0
  88. package/dist/types/providers/google.d.ts +4 -0
  89. package/dist/types/providers/grammar.d.ts +1 -0
  90. package/dist/types/providers/kimi.d.ts +27 -0
  91. package/dist/types/providers/mock.d.ts +179 -0
  92. package/dist/types/providers/ollama.d.ts +8 -0
  93. package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
  94. package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
  95. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  96. package/dist/types/providers/openai-chat-wire.d.ts +669 -0
  97. package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
  98. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  99. package/dist/types/providers/openai-codex-responses.d.ts +238 -0
  100. package/dist/types/providers/openai-completions.d.ts +48 -0
  101. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  102. package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
  103. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  104. package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
  105. package/dist/types/providers/openai-responses.d.ts +131 -0
  106. package/dist/types/providers/openai-shared.d.ts +624 -0
  107. package/dist/types/providers/pi-native-client.d.ts +13 -0
  108. package/dist/types/providers/pi-native-server.d.ts +69 -0
  109. package/dist/types/providers/register-builtins.d.ts +37 -0
  110. package/dist/types/providers/synthetic.d.ts +26 -0
  111. package/dist/types/providers/transform-messages.d.ts +32 -0
  112. package/dist/types/providers/vision-guard.d.ts +20 -0
  113. package/dist/types/registry/aiand.d.ts +7 -0
  114. package/dist/types/registry/aimlapi.d.ts +4 -0
  115. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  116. package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
  117. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  118. package/dist/types/registry/anthropic.d.ts +10 -0
  119. package/dist/types/registry/api-key-login.d.ts +42 -0
  120. package/dist/types/registry/api-key-validation.d.ts +43 -0
  121. package/dist/types/registry/aws.d.ts +13 -0
  122. package/dist/types/registry/azure.d.ts +4 -0
  123. package/dist/types/registry/baseten.d.ts +7 -0
  124. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  125. package/dist/types/registry/cerebras.d.ts +7 -0
  126. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  127. package/dist/types/registry/coreweave.d.ts +7 -0
  128. package/dist/types/registry/cursor.d.ts +7 -0
  129. package/dist/types/registry/deepseek.d.ts +8 -0
  130. package/dist/types/registry/derived.d.ts +5 -0
  131. package/dist/types/registry/devin.d.ts +8 -0
  132. package/dist/types/registry/exa.d.ts +8 -0
  133. package/dist/types/registry/firepass.d.ts +16 -0
  134. package/dist/types/registry/fireworks.d.ts +7 -0
  135. package/dist/types/registry/github-copilot.d.ts +7 -0
  136. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  137. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  138. package/dist/types/registry/gmi-cloud.d.ts +7 -0
  139. package/dist/types/registry/google-antigravity.d.ts +9 -0
  140. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  141. package/dist/types/registry/google-vertex.d.ts +5 -0
  142. package/dist/types/registry/google.d.ts +4 -0
  143. package/dist/types/registry/groq.d.ts +4 -0
  144. package/dist/types/registry/huggingface.d.ts +7 -0
  145. package/dist/types/registry/index.d.ts +4 -0
  146. package/dist/types/registry/kagi.d.ts +14 -0
  147. package/dist/types/registry/kilo.d.ts +7 -0
  148. package/dist/types/registry/kimi-code.d.ts +7 -0
  149. package/dist/types/registry/litellm.d.ts +13 -0
  150. package/dist/types/registry/llama-cpp.d.ts +8 -0
  151. package/dist/types/registry/lm-studio.d.ts +8 -0
  152. package/dist/types/registry/meta.d.ts +7 -0
  153. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  154. package/dist/types/registry/minimax-code.d.ts +6 -0
  155. package/dist/types/registry/minimax.d.ts +4 -0
  156. package/dist/types/registry/mistral.d.ts +4 -0
  157. package/dist/types/registry/moonshot.d.ts +7 -0
  158. package/dist/types/registry/nanogpt.d.ts +7 -0
  159. package/dist/types/registry/novita.d.ts +6 -0
  160. package/dist/types/registry/nvidia.d.ts +7 -0
  161. package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
  162. package/dist/types/registry/oauth/anthropic.d.ts +24 -0
  163. package/dist/types/registry/oauth/callback-server.d.ts +74 -0
  164. package/dist/types/registry/oauth/cursor.d.ts +16 -0
  165. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  166. package/dist/types/registry/oauth/devin.d.ts +5 -0
  167. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  168. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  169. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  170. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  171. package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
  172. package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
  173. package/dist/types/registry/oauth/index.d.ts +45 -0
  174. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  175. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  176. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  177. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  178. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  179. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  180. package/dist/types/registry/oauth/types.d.ts +87 -0
  181. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  182. package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
  183. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  184. package/dist/types/registry/oauth/zai.d.ts +25 -0
  185. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  186. package/dist/types/registry/ollama.d.ts +12 -0
  187. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  188. package/dist/types/registry/openai-codex.d.ts +9 -0
  189. package/dist/types/registry/openai.d.ts +4 -0
  190. package/dist/types/registry/opencode-go.d.ts +6 -0
  191. package/dist/types/registry/opencode-zen.d.ts +6 -0
  192. package/dist/types/registry/openrouter.d.ts +13 -0
  193. package/dist/types/registry/parallel.d.ts +14 -0
  194. package/dist/types/registry/perplexity.d.ts +7 -0
  195. package/dist/types/registry/qianfan.d.ts +7 -0
  196. package/dist/types/registry/qwen-portal.d.ts +7 -0
  197. package/dist/types/registry/registry.d.ts +367 -0
  198. package/dist/types/registry/sakana.d.ts +7 -0
  199. package/dist/types/registry/siliconflow-cn.d.ts +7 -0
  200. package/dist/types/registry/siliconflow.d.ts +7 -0
  201. package/dist/types/registry/synthetic.d.ts +6 -0
  202. package/dist/types/registry/tavily.d.ts +14 -0
  203. package/dist/types/registry/together.d.ts +6 -0
  204. package/dist/types/registry/types.d.ts +75 -0
  205. package/dist/types/registry/umans.d.ts +7 -0
  206. package/dist/types/registry/venice.d.ts +13 -0
  207. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  208. package/dist/types/registry/vllm.d.ts +7 -0
  209. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  210. package/dist/types/registry/xai-oauth.d.ts +7 -0
  211. package/dist/types/registry/xai.d.ts +7 -0
  212. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  213. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  214. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  215. package/dist/types/registry/xiaomi.d.ts +6 -0
  216. package/dist/types/registry/zai.d.ts +15 -0
  217. package/dist/types/registry/zenmux.d.ts +7 -0
  218. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  219. package/dist/types/stream.d.ts +46 -0
  220. package/dist/types/types.d.ts +1064 -0
  221. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  222. package/dist/types/usage/claude.d.ts +4 -0
  223. package/dist/types/usage/cursor.d.ts +4 -0
  224. package/dist/types/usage/gemini.d.ts +2 -0
  225. package/dist/types/usage/github-copilot.d.ts +7 -0
  226. package/dist/types/usage/google-antigravity.d.ts +15 -0
  227. package/dist/types/usage/kimi.d.ts +2 -0
  228. package/dist/types/usage/minimax-code.d.ts +3 -0
  229. package/dist/types/usage/ollama.d.ts +5 -0
  230. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  231. package/dist/types/usage/openai-codex-reset.d.ts +88 -0
  232. package/dist/types/usage/openai-codex.d.ts +10 -0
  233. package/dist/types/usage/opencode-go.d.ts +2 -0
  234. package/dist/types/usage/shared.d.ts +1 -0
  235. package/dist/types/usage/synthetic.d.ts +2 -0
  236. package/dist/types/usage/umans.d.ts +2 -0
  237. package/dist/types/usage/xai-oauth.d.ts +12 -0
  238. package/dist/types/usage/zai.d.ts +3 -0
  239. package/dist/types/usage.d.ts +527 -0
  240. package/dist/types/utils/abort.d.ts +25 -0
  241. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  242. package/dist/types/utils/aws-profile.d.ts +17 -0
  243. package/dist/types/utils/block-symbols.d.ts +62 -0
  244. package/dist/types/utils/deterministic-id.d.ts +16 -0
  245. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  246. package/dist/types/utils/event-stream.d.ts +39 -0
  247. package/dist/types/utils/foundry.d.ts +1 -0
  248. package/dist/types/utils/google-validation.d.ts +2 -0
  249. package/dist/types/utils/harmony-leak.d.ts +135 -0
  250. package/dist/types/utils/http-inspector.d.ts +49 -0
  251. package/dist/types/utils/idle-iterator.d.ts +149 -0
  252. package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
  253. package/dist/types/utils/openai-http.d.ts +48 -0
  254. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  255. package/dist/types/utils/parse-bind.d.ts +23 -0
  256. package/dist/types/utils/provider-response.d.ts +3 -0
  257. package/dist/types/utils/proxy.d.ts +39 -0
  258. package/dist/types/utils/request-debug.d.ts +29 -0
  259. package/dist/types/utils/retry-after.d.ts +4 -0
  260. package/dist/types/utils/retry.d.ts +14 -0
  261. package/dist/types/utils/schema/adapt.d.ts +24 -0
  262. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  263. package/dist/types/utils/schema/dereference.d.ts +11 -0
  264. package/dist/types/utils/schema/draft.d.ts +10 -0
  265. package/dist/types/utils/schema/equality.d.ts +4 -0
  266. package/dist/types/utils/schema/fields.d.ts +54 -0
  267. package/dist/types/utils/schema/index.d.ts +14 -0
  268. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  269. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  270. package/dist/types/utils/schema/normalize.d.ts +153 -0
  271. package/dist/types/utils/schema/spill.d.ts +8 -0
  272. package/dist/types/utils/schema/stamps.d.ts +17 -0
  273. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  274. package/dist/types/utils/schema/types.d.ts +4 -0
  275. package/dist/types/utils/schema/typescript.d.ts +24 -0
  276. package/dist/types/utils/schema/wire.d.ts +52 -0
  277. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  278. package/dist/types/utils/sse-debug.d.ts +5 -0
  279. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  280. package/dist/types/utils/thinking-loop.d.ts +102 -0
  281. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  282. package/dist/types/utils/tool-choice.d.ts +52 -0
  283. package/dist/types/utils/validation.d.ts +28 -0
  284. package/dist/types/utils.d.ts +57 -0
  285. package/package.json +138 -0
  286. package/src/api-registry.ts +109 -0
  287. package/src/auth/sqlite-credential-store.ts +2066 -0
  288. package/src/auth-broker/client.ts +471 -0
  289. package/src/auth-broker/discover.ts +310 -0
  290. package/src/auth-broker/index.ts +7 -0
  291. package/src/auth-broker/refresher.ts +117 -0
  292. package/src/auth-broker/remote-store.ts +1332 -0
  293. package/src/auth-broker/server.ts +898 -0
  294. package/src/auth-broker/snapshot-cache.ts +200 -0
  295. package/src/auth-broker/types.ts +193 -0
  296. package/src/auth-broker/wire-schema-resource.ts +487 -0
  297. package/src/auth-broker/wire-schemas.ts +43 -0
  298. package/src/auth-gateway/http.ts +227 -0
  299. package/src/auth-gateway/index.ts +3 -0
  300. package/src/auth-gateway/server.ts +836 -0
  301. package/src/auth-gateway/types.ts +153 -0
  302. package/src/auth-retry.ts +401 -0
  303. package/src/auth-storage.ts +6540 -0
  304. package/src/dialect/anthropic.md +31 -0
  305. package/src/dialect/anthropic.ts +608 -0
  306. package/src/dialect/catalog.ts +29 -0
  307. package/src/dialect/coercion.ts +136 -0
  308. package/src/dialect/deepseek.md +24 -0
  309. package/src/dialect/deepseek.ts +609 -0
  310. package/src/dialect/demotion.ts +40 -0
  311. package/src/dialect/examples.ts +71 -0
  312. package/src/dialect/factory.ts +34 -0
  313. package/src/dialect/fenced-thinking.ts +184 -0
  314. package/src/dialect/gemini.md +44 -0
  315. package/src/dialect/gemini.ts +583 -0
  316. package/src/dialect/gemma.md +33 -0
  317. package/src/dialect/gemma.ts +387 -0
  318. package/src/dialect/glm.md +32 -0
  319. package/src/dialect/glm.ts +579 -0
  320. package/src/dialect/harmony.md +31 -0
  321. package/src/dialect/harmony.ts +345 -0
  322. package/src/dialect/hermes.md +25 -0
  323. package/src/dialect/hermes.ts +206 -0
  324. package/src/dialect/history.ts +81 -0
  325. package/src/dialect/index.ts +15 -0
  326. package/src/dialect/inventory.ts +30 -0
  327. package/src/dialect/kimi.md +24 -0
  328. package/src/dialect/kimi.ts +340 -0
  329. package/src/dialect/minimax.md +31 -0
  330. package/src/dialect/minimax.ts +95 -0
  331. package/src/dialect/owned-stream.ts +481 -0
  332. package/src/dialect/prompt-template.md +12 -0
  333. package/src/dialect/qwen3.md +28 -0
  334. package/src/dialect/qwen3.ts +240 -0
  335. package/src/dialect/rendering.ts +304 -0
  336. package/src/dialect/thinking.ts +292 -0
  337. package/src/dialect/types.ts +56 -0
  338. package/src/dialect/xml.md +22 -0
  339. package/src/dialect/xml.ts +90 -0
  340. package/src/error/abort.ts +18 -0
  341. package/src/error/auth-classify.ts +47 -0
  342. package/src/error/auth.ts +48 -0
  343. package/src/error/aws.ts +35 -0
  344. package/src/error/classes.ts +281 -0
  345. package/src/error/finalize.ts +69 -0
  346. package/src/error/flags.ts +602 -0
  347. package/src/error/format.ts +45 -0
  348. package/src/error/gateway.ts +96 -0
  349. package/src/error/index.ts +14 -0
  350. package/src/error/oauth.ts +58 -0
  351. package/src/error/provider.ts +63 -0
  352. package/src/error/rate-limit.ts +303 -0
  353. package/src/error/retryable.ts +70 -0
  354. package/src/error/validation.ts +44 -0
  355. package/src/index.ts +51 -0
  356. package/src/provider-details.ts +90 -0
  357. package/src/providers/amazon-bedrock.ts +1064 -0
  358. package/src/providers/anthropic-client.ts +317 -0
  359. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  360. package/src/providers/anthropic-messages-server.ts +818 -0
  361. package/src/providers/anthropic-wire.ts +359 -0
  362. package/src/providers/anthropic.ts +4539 -0
  363. package/src/providers/aws-credentials.ts +772 -0
  364. package/src/providers/aws-eventstream.ts +181 -0
  365. package/src/providers/aws-sigv4.ts +218 -0
  366. package/src/providers/azure-openai-responses.ts +438 -0
  367. package/src/providers/bedrock-mantle.ts +110 -0
  368. package/src/providers/claude-code-fingerprint.ts +20 -0
  369. package/src/providers/cowork-fetch.ts +201 -0
  370. package/src/providers/cursor/exec-modern.ts +496 -0
  371. package/src/providers/cursor/proto/agent.proto +4533 -0
  372. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  373. package/src/providers/cursor/proto/buf.yaml +17 -0
  374. package/src/providers/cursor-pi-args.ts +165 -0
  375. package/src/providers/cursor.ts +4689 -0
  376. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  377. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  378. package/src/providers/devin/proto/buf.yaml +17 -0
  379. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  380. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  381. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  382. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  383. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  384. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  385. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  386. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  387. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  388. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  389. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  390. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  391. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  392. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  393. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  394. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  395. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  396. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  397. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  398. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  399. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  400. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  401. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  402. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  403. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  404. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  405. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  406. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  407. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  408. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  409. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  410. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  411. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  412. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  413. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  414. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  415. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  416. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  417. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  418. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  419. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  420. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  421. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  422. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  423. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  424. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  425. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  426. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  427. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  428. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  429. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  430. package/src/providers/devin.ts +679 -0
  431. package/src/providers/error-message.ts +23 -0
  432. package/src/providers/github-copilot-headers.ts +141 -0
  433. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  434. package/src/providers/gitlab-duo-workflow.ts +3135 -0
  435. package/src/providers/gitlab-duo.ts +399 -0
  436. package/src/providers/google-auth.ts +330 -0
  437. package/src/providers/google-gemini-cli.ts +1370 -0
  438. package/src/providers/google-shared.ts +1122 -0
  439. package/src/providers/google-types.ts +180 -0
  440. package/src/providers/google-vertex.ts +135 -0
  441. package/src/providers/google.ts +47 -0
  442. package/src/providers/grammar.ts +70 -0
  443. package/src/providers/kimi.ts +51 -0
  444. package/src/providers/mock.ts +514 -0
  445. package/src/providers/ollama.ts +776 -0
  446. package/src/providers/openai-anthropic-shim.ts +166 -0
  447. package/src/providers/openai-chat-server-schema.ts +243 -0
  448. package/src/providers/openai-chat-server.ts +752 -0
  449. package/src/providers/openai-chat-wire.ts +859 -0
  450. package/src/providers/openai-codex/request-transformer.ts +491 -0
  451. package/src/providers/openai-codex/response-handler.ts +102 -0
  452. package/src/providers/openai-codex-responses.ts +4716 -0
  453. package/src/providers/openai-completions.ts +2389 -0
  454. package/src/providers/openai-reasoning-fallback.ts +269 -0
  455. package/src/providers/openai-responses-server-schema.ts +397 -0
  456. package/src/providers/openai-responses-server.ts +1466 -0
  457. package/src/providers/openai-responses-wire.ts +6416 -0
  458. package/src/providers/openai-responses.ts +1393 -0
  459. package/src/providers/openai-shared.ts +3500 -0
  460. package/src/providers/pi-native-client.ts +275 -0
  461. package/src/providers/pi-native-server.ts +245 -0
  462. package/src/providers/register-builtins.ts +503 -0
  463. package/src/providers/synthetic.ts +50 -0
  464. package/src/providers/transform-messages.ts +1083 -0
  465. package/src/providers/vision-guard.ts +54 -0
  466. package/src/registry/aiand.ts +22 -0
  467. package/src/registry/aimlapi.ts +6 -0
  468. package/src/registry/alibaba-coding-plan.ts +104 -0
  469. package/src/registry/alibaba-token-plan.ts +125 -0
  470. package/src/registry/amazon-bedrock.ts +22 -0
  471. package/src/registry/anthropic.ts +26 -0
  472. package/src/registry/api-key-login.ts +115 -0
  473. package/src/registry/api-key-validation.ts +145 -0
  474. package/src/registry/aws.ts +57 -0
  475. package/src/registry/azure.ts +6 -0
  476. package/src/registry/baseten.ts +22 -0
  477. package/src/registry/bedrock-mantle.ts +34 -0
  478. package/src/registry/cerebras.ts +23 -0
  479. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  480. package/src/registry/coreweave.ts +40 -0
  481. package/src/registry/cursor.ts +20 -0
  482. package/src/registry/deepseek.ts +46 -0
  483. package/src/registry/derived.ts +9 -0
  484. package/src/registry/devin.ts +15 -0
  485. package/src/registry/exa.ts +19 -0
  486. package/src/registry/firepass.ts +32 -0
  487. package/src/registry/fireworks.ts +28 -0
  488. package/src/registry/github-copilot.ts +22 -0
  489. package/src/registry/gitlab-duo-workflow.ts +20 -0
  490. package/src/registry/gitlab-duo.ts +19 -0
  491. package/src/registry/gmi-cloud.ts +22 -0
  492. package/src/registry/google-antigravity.ts +22 -0
  493. package/src/registry/google-gemini-cli.ts +22 -0
  494. package/src/registry/google-vertex.ts +38 -0
  495. package/src/registry/google.ts +6 -0
  496. package/src/registry/groq.ts +6 -0
  497. package/src/registry/huggingface.ts +29 -0
  498. package/src/registry/index.ts +4 -0
  499. package/src/registry/kagi.ts +46 -0
  500. package/src/registry/kilo.ts +114 -0
  501. package/src/registry/kimi-code.ts +17 -0
  502. package/src/registry/litellm.ts +45 -0
  503. package/src/registry/llama-cpp.ts +35 -0
  504. package/src/registry/lm-studio.ts +31 -0
  505. package/src/registry/meta.ts +22 -0
  506. package/src/registry/minimax-code-cn.ts +12 -0
  507. package/src/registry/minimax-code.ts +12 -0
  508. package/src/registry/minimax.ts +6 -0
  509. package/src/registry/mistral.ts +6 -0
  510. package/src/registry/moonshot.ts +28 -0
  511. package/src/registry/nanogpt.ts +22 -0
  512. package/src/registry/novita.ts +25 -0
  513. package/src/registry/nvidia.ts +61 -0
  514. package/src/registry/oauth/anthropic-constants.ts +12 -0
  515. package/src/registry/oauth/anthropic.ts +346 -0
  516. package/src/registry/oauth/callback-server.ts +438 -0
  517. package/src/registry/oauth/cursor.ts +187 -0
  518. package/src/registry/oauth/device-code.ts +92 -0
  519. package/src/registry/oauth/devin.ts +124 -0
  520. package/src/registry/oauth/github-copilot.ts +369 -0
  521. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  522. package/src/registry/oauth/gitlab-duo.ts +222 -0
  523. package/src/registry/oauth/google-antigravity.ts +225 -0
  524. package/src/registry/oauth/google-gemini-cli.ts +297 -0
  525. package/src/registry/oauth/google-oauth-shared.ts +211 -0
  526. package/src/registry/oauth/index.ts +187 -0
  527. package/src/registry/oauth/kimi.ts +297 -0
  528. package/src/registry/oauth/minimax-code.ts +53 -0
  529. package/src/registry/oauth/oauth.html +317 -0
  530. package/src/registry/oauth/openai-codex.ts +384 -0
  531. package/src/registry/oauth/opencode.ts +50 -0
  532. package/src/registry/oauth/perplexity.ts +228 -0
  533. package/src/registry/oauth/pkce.ts +18 -0
  534. package/src/registry/oauth/types.ts +96 -0
  535. package/src/registry/oauth/wafer.ts +24 -0
  536. package/src/registry/oauth/xai-oauth.ts +559 -0
  537. package/src/registry/oauth/xiaomi.ts +211 -0
  538. package/src/registry/oauth/zai.ts +285 -0
  539. package/src/registry/ollama-cloud.ts +36 -0
  540. package/src/registry/ollama.ts +43 -0
  541. package/src/registry/openai-codex-device.ts +18 -0
  542. package/src/registry/openai-codex.ts +19 -0
  543. package/src/registry/openai.ts +6 -0
  544. package/src/registry/opencode-go.ts +12 -0
  545. package/src/registry/opencode-zen.ts +12 -0
  546. package/src/registry/openrouter.ts +28 -0
  547. package/src/registry/parallel.ts +45 -0
  548. package/src/registry/perplexity.ts +13 -0
  549. package/src/registry/qianfan.ts +27 -0
  550. package/src/registry/qwen-portal.ts +50 -0
  551. package/src/registry/registry.ts +182 -0
  552. package/src/registry/sakana.ts +22 -0
  553. package/src/registry/siliconflow-cn.ts +22 -0
  554. package/src/registry/siliconflow.ts +22 -0
  555. package/src/registry/synthetic.ts +21 -0
  556. package/src/registry/tavily.ts +45 -0
  557. package/src/registry/together.ts +22 -0
  558. package/src/registry/types.ts +86 -0
  559. package/src/registry/umans.ts +23 -0
  560. package/src/registry/venice.ts +33 -0
  561. package/src/registry/vercel-ai-gateway.ts +38 -0
  562. package/src/registry/vllm.ts +34 -0
  563. package/src/registry/wafer-serverless.ts +12 -0
  564. package/src/registry/xai-oauth.ts +17 -0
  565. package/src/registry/xai.ts +22 -0
  566. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  567. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  568. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  569. package/src/registry/xiaomi.ts +12 -0
  570. package/src/registry/zai.ts +41 -0
  571. package/src/registry/zenmux.ts +22 -0
  572. package/src/registry/zhipu-coding-plan.ts +27 -0
  573. package/src/stream.ts +1944 -0
  574. package/src/types.ts +1243 -0
  575. package/src/usage/alibaba-token-plan.ts +230 -0
  576. package/src/usage/claude.ts +830 -0
  577. package/src/usage/cursor.ts +335 -0
  578. package/src/usage/gemini.ts +258 -0
  579. package/src/usage/github-copilot.ts +424 -0
  580. package/src/usage/google-antigravity.ts +497 -0
  581. package/src/usage/kimi.ts +277 -0
  582. package/src/usage/minimax-code.ts +291 -0
  583. package/src/usage/ollama.ts +41 -0
  584. package/src/usage/openai-codex-base-url.ts +35 -0
  585. package/src/usage/openai-codex-reset.ts +205 -0
  586. package/src/usage/openai-codex.ts +627 -0
  587. package/src/usage/opencode-go.ts +89 -0
  588. package/src/usage/shared.ts +10 -0
  589. package/src/usage/synthetic.ts +180 -0
  590. package/src/usage/umans.ts +192 -0
  591. package/src/usage/xai-oauth.ts +414 -0
  592. package/src/usage/zai.ts +370 -0
  593. package/src/usage.ts +411 -0
  594. package/src/utils/abort.ts +67 -0
  595. package/src/utils/anthropic-auth.ts +93 -0
  596. package/src/utils/aws-profile.ts +88 -0
  597. package/src/utils/block-symbols.ts +78 -0
  598. package/src/utils/deterministic-id.ts +20 -0
  599. package/src/utils/empty-completion-retry.ts +161 -0
  600. package/src/utils/event-stream.ts +202 -0
  601. package/src/utils/foundry.ts +8 -0
  602. package/src/utils/google-validation.ts +25 -0
  603. package/src/utils/harmony-leak.ts +500 -0
  604. package/src/utils/http-inspector.ts +196 -0
  605. package/src/utils/idle-iterator.ts +531 -0
  606. package/src/utils/leaked-thinking-stream.ts +483 -0
  607. package/src/utils/openai-http.ts +119 -0
  608. package/src/utils/openrouter-headers.ts +12 -0
  609. package/src/utils/parse-bind.ts +56 -0
  610. package/src/utils/provider-response.ts +30 -0
  611. package/src/utils/proxy.ts +314 -0
  612. package/src/utils/request-debug.ts +351 -0
  613. package/src/utils/retry-after.ts +121 -0
  614. package/src/utils/retry.ts +77 -0
  615. package/src/utils/schema/CONSTRAINTS.md +168 -0
  616. package/src/utils/schema/adapt.ts +36 -0
  617. package/src/utils/schema/compatibility.ts +435 -0
  618. package/src/utils/schema/dereference.ts +98 -0
  619. package/src/utils/schema/draft.ts +341 -0
  620. package/src/utils/schema/equality.ts +97 -0
  621. package/src/utils/schema/fields.ts +210 -0
  622. package/src/utils/schema/index.ts +14 -0
  623. package/src/utils/schema/json-schema-validator.ts +595 -0
  624. package/src/utils/schema/meta-validator.ts +167 -0
  625. package/src/utils/schema/normalize.ts +2314 -0
  626. package/src/utils/schema/spill.ts +43 -0
  627. package/src/utils/schema/stamps.ts +109 -0
  628. package/src/utils/schema/strict-tool-validation.ts +117 -0
  629. package/src/utils/schema/types.ts +10 -0
  630. package/src/utils/schema/typescript.ts +212 -0
  631. package/src/utils/schema/wire.ts +662 -0
  632. package/src/utils/sdk-stream-timeout.ts +43 -0
  633. package/src/utils/sse-debug.ts +18 -0
  634. package/src/utils/stream-markup-healing.ts +247 -0
  635. package/src/utils/thinking-loop.ts +552 -0
  636. package/src/utils/tool-call-loop-guard.ts +107 -0
  637. package/src/utils/tool-choice.ts +101 -0
  638. package/src/utils/validation.ts +1932 -0
  639. package/src/utils.ts +492 -0
@@ -0,0 +1,1332 @@
1
+ /**
2
+ * Client-side {@link AuthCredentialStore} that mirrors a remote broker's
3
+ * snapshot. Refresh tokens never leave the broker; mutating methods (`replace*`,
4
+ * `upsert*`, `delete*ForProvider`) throw because login flows are server-side.
5
+ *
6
+ * Cache (`getCache`/`setCache`/`cleanExpiredCache`) is in-memory and ephemeral —
7
+ * usage reports cache TTL is 5 minutes per credential, so durability across
8
+ * runs isn't required.
9
+ */
10
+ import * as os from "node:os";
11
+ import { scheduler } from "node:timers/promises";
12
+ import { getInstallId, logger } from "@linxiraos/pi-utils";
13
+ import {
14
+ type AuthCredential,
15
+ type AuthCredentialSnapshotEntry,
16
+ type AuthCredentialStore,
17
+ type DisabledCredentialSummary,
18
+ type OAuthCredential,
19
+ REMOTE_REFRESH_SENTINEL,
20
+ type StoredAuthCredential,
21
+ type StoredCredentialBlock,
22
+ } from "../auth-storage";
23
+ import * as AIError from "../error";
24
+ import type { OAuthCredentials } from "../registry/oauth/types";
25
+ import type { Provider } from "../types";
26
+ import type { ObservedUsageEntry, UsageReport } from "../usage";
27
+ import { type AuthBrokerClient, AuthBrokerError, AuthBrokerStreamUnsupportedError } from "./client";
28
+ import type {
29
+ CredentialBlockSnapshot,
30
+ RefresherSchedule,
31
+ SnapshotEntry,
32
+ SnapshotResponse,
33
+ SnapshotStreamEvent,
34
+ } from "./types";
35
+
36
+ /**
37
+ * Per-provider OAuth identities visible to this trusted broker client.
38
+ * Missing providers are unrestricted; an empty set excludes that provider's
39
+ * OAuth credentials. API keys are never filtered.
40
+ */
41
+ export type AuthBrokerAccountPool = ReadonlyMap<string, ReadonlySet<string>>;
42
+
43
+ function isCredentialInAccountPool(
44
+ entry: Pick<SnapshotEntry, "provider" | "credential" | "identityKey">,
45
+ accountPool: AuthBrokerAccountPool | undefined,
46
+ ): boolean {
47
+ if (entry.credential.type !== "oauth") return true;
48
+ const identities = accountPool?.get(entry.provider);
49
+ if (identities === undefined) return true;
50
+ return entry.identityKey !== null && identities.has(entry.identityKey);
51
+ }
52
+
53
+ /**
54
+ * Client-side TTL for the aggregate `/v1/usage` response. The broker dedups
55
+ * upstream `/usage` hits via AuthStorage's 5-minute per-credential cache plus
56
+ * single-flight, so this short client TTL mainly folds the parallel fan-out
57
+ * from `#rankOAuthSelections` into a single round-trip — a ranking pass issues
58
+ * one broker call instead of N.
59
+ */
60
+ const USAGE_CACHE_TTL_MS = 15_000;
61
+ const CREDENTIAL_BLOCK_RECONCILE_DELAY_MS = 5 * 60_000;
62
+ const WAIT_THRESHOLD_MS = 1_000;
63
+ const MAX_WAIT_MS = 5_000;
64
+ const BACKGROUND_WAIT_MS = 30_000;
65
+ const BACKGROUND_BACKOFF_INITIAL_MS = 500;
66
+ const BACKGROUND_BACKOFF_MAX_MS = 30_000;
67
+
68
+ function compareCredentialBlockSnapshots(a: CredentialBlockSnapshot, b: CredentialBlockSnapshot): number {
69
+ const provider = a.providerKey.localeCompare(b.providerKey);
70
+ if (provider !== 0) return provider;
71
+ const scope = a.blockScope.localeCompare(b.blockScope);
72
+ if (scope !== 0) return scope;
73
+ const blockedUntil = a.blockedUntilMs - b.blockedUntilMs;
74
+ if (blockedUntil !== 0) return blockedUntil;
75
+ return (a.updatedAtMs ?? 0) - (b.updatedAtMs ?? 0);
76
+ }
77
+
78
+ function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBlockSnapshot {
79
+ return {
80
+ providerKey: block.providerKey,
81
+ blockScope: block.blockScope,
82
+ blockedUntilMs: block.blockedUntilMs,
83
+ ...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
84
+ };
85
+ }
86
+
87
+ function credentialBlockSnapshotsEqual(
88
+ left: readonly CredentialBlockSnapshot[] | undefined,
89
+ right: readonly CredentialBlockSnapshot[] | undefined,
90
+ ): boolean {
91
+ const leftBlocks = left ?? [];
92
+ const rightBlocks = right ?? [];
93
+ if (leftBlocks.length !== rightBlocks.length) return false;
94
+ for (let index = 0; index < leftBlocks.length; index += 1) {
95
+ const leftBlock = leftBlocks[index]!;
96
+ const rightBlock = rightBlocks[index]!;
97
+ if (
98
+ leftBlock.providerKey !== rightBlock.providerKey ||
99
+ leftBlock.blockScope !== rightBlock.blockScope ||
100
+ leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs ||
101
+ leftBlock.updatedAtMs !== rightBlock.updatedAtMs
102
+ ) {
103
+ return false;
104
+ }
105
+ }
106
+ return true;
107
+ }
108
+
109
+ function snapshotBlocksChanged(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[]): boolean {
110
+ const previousBlocksById = new Map<number, readonly CredentialBlockSnapshot[] | undefined>();
111
+ for (const entry of previous) previousBlocksById.set(entry.id, entry.blocks);
112
+ for (const entry of next) {
113
+ const previousBlocks = previousBlocksById.get(entry.id);
114
+ if (!credentialBlockSnapshotsEqual(previousBlocks, entry.blocks)) return true;
115
+ previousBlocksById.delete(entry.id);
116
+ }
117
+ for (const previousBlocks of previousBlocksById.values()) {
118
+ if (previousBlocks && previousBlocks.length > 0) return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function credentialEntryWithBlocks(
124
+ entry: AuthCredentialSnapshotEntry,
125
+ blocks: readonly CredentialBlockSnapshot[] | undefined,
126
+ ): SnapshotEntry {
127
+ const incoming: SnapshotEntry = { ...entry, rotatesInMs: null };
128
+ if (blocks && blocks.length > 0) incoming.blocks = [...blocks].sort(compareCredentialBlockSnapshots);
129
+ return incoming;
130
+ }
131
+
132
+ function emptySnapshot(): SnapshotResponse {
133
+ return {
134
+ generation: 0,
135
+ generatedAt: 0,
136
+ serverNowMs: 0,
137
+ refresher: {
138
+ enabled: false,
139
+ intervalMs: 0,
140
+ skewMs: 0,
141
+ nextSweepInMs: Number.MAX_SAFE_INTEGER,
142
+ },
143
+ credentials: [],
144
+ };
145
+ }
146
+
147
+ interface CacheEntry {
148
+ value: string;
149
+ expiresAtSec: number;
150
+ }
151
+
152
+ interface UsageCacheEntry {
153
+ /**
154
+ * `null` means the last aggregate `/v1/usage` fetch failed. Callers treat
155
+ * this the same as a successful empty-report response ("no usage signal
156
+ * for this cycle"), and the same 15s TTL applies so transient broker
157
+ * outages don't turn every ranking pass into a broker retry storm.
158
+ */
159
+ reports: UsageReport[] | null;
160
+ fetchedAt: number;
161
+ }
162
+
163
+ function usageOverlayKey(
164
+ provider: Provider,
165
+ ids: { accountId?: string; email?: string; projectId?: string; orgId?: string },
166
+ ): string | undefined {
167
+ // Org first: one account email can hold several organizations (Anthropic
168
+ // Team seat + personal Max), each with its own limit pools. Keying the
169
+ // overlay by account/email would merge the two pools' header ingests.
170
+ // But the org alone is not enough either: two Team members share the org
171
+ // id while drawing on per-user pools, so the key stays qualified by the
172
+ // member's own base identity whenever one is known.
173
+ let base: string | undefined;
174
+ const accountId = ids.accountId?.trim().toLowerCase();
175
+ const email = ids.email?.trim().toLowerCase();
176
+ const projectId = ids.projectId?.trim().toLowerCase();
177
+ if (accountId) base = `account:${accountId}`;
178
+ else if (email) base = `email:${email}`;
179
+ else if (projectId) base = `project:${projectId}`;
180
+ const orgId = ids.orgId?.trim().toLowerCase();
181
+ if (orgId) return base ? `${provider}\0org:${orgId}|${base}` : `${provider}\0org:${orgId}`;
182
+ if (base) return `${provider}\0${base}`;
183
+ return undefined;
184
+ }
185
+
186
+ function mergeUsageReports(base: UsageReport, overlay: UsageReport): UsageReport {
187
+ const overlayLimitsById = new Map(overlay.limits.map(limit => [limit.id, limit]));
188
+ const limits = [];
189
+ for (const limit of base.limits) {
190
+ const replacement = overlayLimitsById.get(limit.id);
191
+ if (replacement) {
192
+ limits.push(replacement);
193
+ overlayLimitsById.delete(limit.id);
194
+ } else {
195
+ limits.push(limit);
196
+ }
197
+ }
198
+ for (const limit of overlayLimitsById.values()) limits.push(limit);
199
+ const overlayMetadata = (overlay.metadata ?? {}) as Record<string, unknown>;
200
+ return {
201
+ ...base,
202
+ fetchedAt: Math.max(base.fetchedAt, overlay.fetchedAt),
203
+ limits,
204
+ metadata: {
205
+ ...overlayMetadata,
206
+ ...(base.metadata ?? {}),
207
+ ...(overlayMetadata.headersUpdatedAt !== undefined
208
+ ? { headersUpdatedAt: overlayMetadata.headersUpdatedAt }
209
+ : {}),
210
+ },
211
+ };
212
+ }
213
+
214
+ export interface RemoteAuthCredentialStoreOptions {
215
+ client: AuthBrokerClient;
216
+ /**
217
+ * Initial snapshot. When omitted, callers must call
218
+ * {@link RemoteAuthCredentialStore.refreshSnapshot} before the first read.
219
+ */
220
+ initialSnapshot?: SnapshotResponse;
221
+ /**
222
+ * Subscribe to the broker's SSE snapshot stream when available. Falls back
223
+ * to long-poll permanently when the broker returns 404. Default `true`.
224
+ */
225
+ streamSnapshots?: boolean;
226
+ /**
227
+ * Called with each broker-sourced raw full snapshot after the filtered
228
+ * public view is applied. The constructor's initial snapshot intentionally
229
+ * does not trigger this hook.
230
+ */
231
+ onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
232
+ /**
233
+ * OAuth identities visible through this store. This is a trusted-client
234
+ * routing policy, not broker authorization.
235
+ */
236
+ accountPool?: AuthBrokerAccountPool;
237
+ /** Flush cadence for batched observed-usage reports. Default 10s. */
238
+ observedUsageFlushMs?: number;
239
+ }
240
+
241
+ export class RemoteAuthCredentialStore implements AuthCredentialStore {
242
+ readonly #client: AuthBrokerClient;
243
+ readonly #streamSnapshots: boolean;
244
+ readonly #onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
245
+ readonly #accountPool?: AuthBrokerAccountPool;
246
+ #snapshot: SnapshotResponse = emptySnapshot();
247
+ #snapshotReceivedAt = Date.now();
248
+ #generation = 0;
249
+ #usageOverlays: Map<string, UsageReport> = new Map();
250
+ #backgroundAbort = new AbortController();
251
+ #cache: Map<string, CacheEntry> = new Map();
252
+ #usageCache?: UsageCacheEntry;
253
+ #usageInflight?: Promise<UsageReport[] | null>;
254
+ #credentialBlockReconcileAfter: Map<string, number> = new Map();
255
+ #usageCacheEpoch = 0;
256
+ /** Per-snapshot lookup of oauth credentials by provider; rebuilt when `#snapshot` is replaced. */
257
+ #usageFilterLookup?: { snapshot: SnapshotResponse; byProvider: Map<Provider, OAuthCredential[]> };
258
+ /** Memoized `#filterUsageReports` output, keyed on (input identity, lookup identity). */
259
+ #usageFilterResult?: { input: UsageReport[]; byProvider: Map<Provider, OAuthCredential[]>; output: UsageReport[] };
260
+ #closed = false;
261
+ /**
262
+ * `true` once the SSE consumer received its first frame and hasn't dropped
263
+ * since. Writes consult this to suppress the otherwise-mandatory
264
+ * `refreshSnapshot()` follow-up — the stream will deliver the new
265
+ * generation without an extra GET.
266
+ */
267
+ #streamingActive = false;
268
+ /** Latched once the broker has answered 404 — never try the stream again. */
269
+ #streamingUnsupported = false;
270
+ /** Pending observed usage keyed by `provider\u0000model`, merged until flush. */
271
+ #observedUsage = new Map<string, ObservedUsageEntry>();
272
+ #observedUsageTimer: Timer | undefined;
273
+ readonly #observedUsageFlushMs: number;
274
+ /** Latched once the broker answered 404 — old broker, never report again. */
275
+ #observedUsageUnsupported = false;
276
+
277
+ constructor(opts: RemoteAuthCredentialStoreOptions) {
278
+ this.#client = opts.client;
279
+ this.#streamSnapshots = opts.streamSnapshots ?? true;
280
+ this.#observedUsageFlushMs = opts.observedUsageFlushMs ?? 10_000;
281
+ this.#accountPool = opts.accountPool
282
+ ? new Map([...opts.accountPool].map(([provider, identities]) => [provider, new Set(identities)]))
283
+ : undefined;
284
+ this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
285
+ this.#onSnapshot = opts.onSnapshot;
286
+ void this.#runBackground();
287
+ }
288
+
289
+ get client(): AuthBrokerClient {
290
+ return this.#client;
291
+ }
292
+
293
+ get snapshot(): SnapshotResponse {
294
+ return this.#snapshot;
295
+ }
296
+
297
+ #applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
298
+ const nowMs = Date.now();
299
+ const previousCredentials = this.#snapshot.credentials;
300
+ const credentials = snapshot.credentials
301
+ .filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
302
+ .map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
303
+ if (snapshotBlocksChanged(previousCredentials, credentials)) this.#invalidateUsageCache();
304
+ if (protectNewBlocks) this.#protectNewSnapshotBlocks(previousCredentials, credentials, nowMs);
305
+ this.#snapshot = { ...snapshot, credentials };
306
+ this.#generation = generation;
307
+ this.#snapshotReceivedAt = nowMs;
308
+ const onSnapshot = this.#onSnapshot;
309
+ if (!onSnapshot) return;
310
+ try {
311
+ onSnapshot(snapshot, generation);
312
+ } catch (error) {
313
+ logger.debug("auth-broker snapshot callback failed", { error: String(error) });
314
+ }
315
+ }
316
+ #protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
317
+ const previousBlocksByKey = new Map<string, string>();
318
+ for (const entry of previous) {
319
+ for (const block of entry.blocks ?? []) {
320
+ previousBlocksByKey.set(
321
+ `${entry.id}\0${block.providerKey}\0${block.blockScope}`,
322
+ `${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`,
323
+ );
324
+ }
325
+ }
326
+ const activeKeys = new Set<string>();
327
+ for (const entry of next) {
328
+ for (const block of entry.blocks ?? []) {
329
+ const key = `${entry.id}\0${block.providerKey}\0${block.blockScope}`;
330
+ activeKeys.add(key);
331
+ const signature = `${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`;
332
+ if (previousBlocksByKey.get(key) === signature) continue;
333
+ const updatedAtMs = block.updatedAtMs ?? nowMs;
334
+ this.#credentialBlockReconcileAfter.set(
335
+ key,
336
+ Math.min(block.blockedUntilMs, updatedAtMs + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
337
+ );
338
+ }
339
+ }
340
+ for (const key of this.#credentialBlockReconcileAfter.keys()) {
341
+ if (!activeKeys.has(key)) this.#credentialBlockReconcileAfter.delete(key);
342
+ }
343
+ }
344
+
345
+ async #runBackground(): Promise<void> {
346
+ let backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
347
+ while (!this.#closed && !this.#backgroundAbort.signal.aborted) {
348
+ if (this.#streamSnapshots && !this.#streamingUnsupported) {
349
+ try {
350
+ await this.#consumeSnapshotStream();
351
+ backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
352
+ continue;
353
+ } catch (error) {
354
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
355
+ if (error instanceof AuthBrokerStreamUnsupportedError) {
356
+ this.#streamingUnsupported = true;
357
+ logger.debug("auth-broker snapshot stream unsupported; falling back to long-poll");
358
+ continue;
359
+ }
360
+ logger.debug("auth-broker snapshot stream failed; backing off", { error: String(error) });
361
+ await scheduler.wait(backoffMs, { signal: this.#backgroundAbort.signal }).catch(() => {});
362
+ backoffMs = Math.min(BACKGROUND_BACKOFF_MAX_MS, backoffMs * 2);
363
+ continue;
364
+ }
365
+ }
366
+ try {
367
+ const result = await this.#client.fetchSnapshot({
368
+ ifGenerationGt: this.#generation,
369
+ waitMs: BACKGROUND_WAIT_MS,
370
+ signal: this.#backgroundAbort.signal,
371
+ });
372
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
373
+ backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
374
+ } catch (error) {
375
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
376
+ logger.debug("auth-broker background snapshot sync failed", { error: String(error) });
377
+ await scheduler.wait(backoffMs, { signal: this.#backgroundAbort.signal }).catch(() => {});
378
+ backoffMs = Math.min(BACKGROUND_BACKOFF_MAX_MS, backoffMs * 2);
379
+ }
380
+ }
381
+ }
382
+
383
+ async #consumeSnapshotStream(): Promise<void> {
384
+ const iterator = this.#client.openSnapshotStream({ signal: this.#backgroundAbort.signal });
385
+ try {
386
+ for await (const event of iterator) {
387
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
388
+ this.#streamingActive = true;
389
+ this.#applyStreamEvent(event);
390
+ }
391
+ } finally {
392
+ this.#streamingActive = false;
393
+ }
394
+ }
395
+
396
+ #applyStreamEvent(event: SnapshotStreamEvent): void {
397
+ switch (event.kind) {
398
+ case "snapshot": {
399
+ // Strip the discriminator so we store the wire-shape SnapshotResponse.
400
+ const { kind: _kind, ...snapshot } = event;
401
+ if (snapshot.generation < this.#generation) {
402
+ logger.debug("auth-broker stream snapshot older than local; ignoring", {
403
+ local: this.#generation,
404
+ incoming: snapshot.generation,
405
+ });
406
+ return;
407
+ }
408
+ this.#applySnapshot(snapshot, snapshot.generation);
409
+ return;
410
+ }
411
+ case "entry": {
412
+ if (event.generation < this.#generation) return;
413
+ this.#applyStreamEntry(event.entry, event.refresher, event.generation, event.serverNowMs);
414
+ return;
415
+ }
416
+ case "removed": {
417
+ if (event.generation < this.#generation) return;
418
+ this.#removeStreamCredential(event.id, event.refresher, event.generation, event.serverNowMs);
419
+ return;
420
+ }
421
+ }
422
+ }
423
+
424
+ #applyStreamEntry(
425
+ entry: SnapshotEntry,
426
+ refresher: RefresherSchedule,
427
+ generation: number,
428
+ serverNowMs: number,
429
+ ): void {
430
+ if (!isCredentialInAccountPool(entry, this.#accountPool)) {
431
+ this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs);
432
+ return;
433
+ }
434
+ const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
435
+ const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
436
+ const previousBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
437
+ const blocksChanged = !credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks);
438
+ if (blocksChanged) this.#invalidateUsageCache();
439
+ const credentials =
440
+ index === -1
441
+ ? [...this.#snapshot.credentials, incoming]
442
+ : this.#snapshot.credentials.map((candidate, i) => (i === index ? incoming : candidate));
443
+ if (blocksChanged) this.#protectNewSnapshotBlocks(this.#snapshot.credentials, credentials, Date.now());
444
+ this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
445
+ this.#generation = generation;
446
+ this.#snapshotReceivedAt = Date.now();
447
+ }
448
+
449
+ #removeStreamCredential(id: number, refresher: RefresherSchedule, generation: number, serverNowMs: number): void {
450
+ const removed = this.#snapshot.credentials.find(entry => entry.id === id);
451
+ if (removed?.blocks && removed.blocks.length > 0) this.#invalidateUsageCache();
452
+ const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
453
+ this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
454
+ this.#generation = generation;
455
+ this.#snapshotReceivedAt = Date.now();
456
+ }
457
+
458
+ /** Re-hydrate the in-memory snapshot from the broker. */
459
+ async refreshSnapshot(): Promise<SnapshotResponse> {
460
+ const result = await this.#client.fetchSnapshot();
461
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
462
+ return this.#snapshot;
463
+ }
464
+
465
+ listAuthCredentials(provider?: string): StoredAuthCredential[] {
466
+ const out: StoredAuthCredential[] = [];
467
+ for (const entry of this.#snapshot.credentials) {
468
+ if (provider !== undefined && entry.provider !== provider) continue;
469
+ out.push({
470
+ id: entry.id,
471
+ provider: entry.provider,
472
+ credential: entry.credential as AuthCredential,
473
+ disabledCause: null,
474
+ });
475
+ }
476
+ return out;
477
+ }
478
+
479
+ /** Broker-backed disabled tombstones; empty against brokers predating the endpoint. */
480
+ listDisabledCredentials(provider?: string, signal?: AbortSignal): Promise<DisabledCredentialSummary[]> {
481
+ return this.#client.listDisabledCredentials(provider, signal);
482
+ }
483
+
484
+ getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
485
+ const nowMs = Date.now();
486
+ this.cleanExpiredCredentialBlocks(nowMs);
487
+ const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
488
+ if (!entry?.blocks) return undefined;
489
+ const block = entry.blocks.find(
490
+ candidate => candidate.providerKey === providerKey && candidate.blockScope === blockScope,
491
+ );
492
+ if (!block || block.blockedUntilMs <= nowMs) return undefined;
493
+ return block.blockedUntilMs;
494
+ }
495
+
496
+ getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
497
+ if (this.getCredentialBlock(credentialId, providerKey, blockScope) === undefined) return undefined;
498
+ return this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`);
499
+ }
500
+
501
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
502
+ const nowMs = Date.now();
503
+ this.cleanExpiredCredentialBlocks(nowMs);
504
+ const ids = new Set(credentialIds);
505
+ const blocks: StoredCredentialBlock[] = [];
506
+ for (const entry of this.#snapshot.credentials) {
507
+ if (!ids.has(entry.id) || !entry.blocks) continue;
508
+ for (const block of entry.blocks) {
509
+ if (block.blockedUntilMs <= nowMs) continue;
510
+ blocks.push({
511
+ credentialId: entry.id,
512
+ providerKey: block.providerKey,
513
+ blockScope: block.blockScope,
514
+ blockedUntilMs: block.blockedUntilMs,
515
+ updatedAtMs: block.updatedAtMs,
516
+ });
517
+ }
518
+ }
519
+ blocks.sort((a, b) => a.credentialId - b.credentialId || compareCredentialBlockSnapshots(a, b));
520
+ return blocks;
521
+ }
522
+
523
+ upsertCredentialBlock(block: StoredCredentialBlock): void {
524
+ this.#upsertSnapshotBlock(block);
525
+ this.#invalidateUsageCache();
526
+ this.#credentialBlockReconcileAfter.set(
527
+ `${block.credentialId}\0${block.providerKey}\0${block.blockScope}`,
528
+ Math.min(block.blockedUntilMs, Date.now() + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
529
+ );
530
+ const body = toCredentialBlockSnapshot(block);
531
+ void this.#client
532
+ .upsertCredentialBlock(block.credentialId, body)
533
+ .then(() => {
534
+ this.#maybeRefreshSnapshot("credential block");
535
+ })
536
+ .catch(error => {
537
+ logger.warn("auth-broker credential block propagation failed", {
538
+ id: block.credentialId,
539
+ providerKey: block.providerKey,
540
+ blockScope: block.blockScope,
541
+ error: String(error),
542
+ });
543
+ });
544
+ }
545
+
546
+ deleteCredentialBlock(_credentialId: number, _providerKey: string, _blockScope: string): void {
547
+ // The broker protocol only supports deleting every block for a credential.
548
+ // Keep scoped blocks until expiry rather than risk deleting unrelated or
549
+ // newer broker state through that broader operation.
550
+ }
551
+
552
+ deleteCredentialBlocks(credentialId: number): void {
553
+ this.#deleteSnapshotBlocks(credentialId);
554
+ for (const key of this.#credentialBlockReconcileAfter.keys()) {
555
+ if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
556
+ }
557
+ this.#invalidateUsageCache();
558
+ void this.#client
559
+ .deleteCredentialBlocks(credentialId)
560
+ .then(() => {
561
+ this.#maybeRefreshSnapshot("credential blocks delete");
562
+ })
563
+ .catch(error => {
564
+ logger.warn("auth-broker credential blocks delete propagation failed", {
565
+ id: credentialId,
566
+ error: String(error),
567
+ });
568
+ });
569
+ }
570
+
571
+ cleanExpiredCredentialBlocks(nowMs: number): void {
572
+ this.#pruneExpiredCredentialBlocks(nowMs);
573
+ for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
574
+ if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * In-memory update from a successful refresh through the broker. AuthStorage
580
+ * calls this after `#replaceCredentialAt`; the broker already persisted the
581
+ * authoritative row, so we just mirror it.
582
+ */
583
+ updateAuthCredential(id: number, credential: AuthCredential): void {
584
+ for (const entry of this.#snapshot.credentials) {
585
+ if (entry.id !== id) continue;
586
+ entry.credential = credential as typeof entry.credential;
587
+ return;
588
+ }
589
+ }
590
+
591
+ deleteAuthCredential(id: number, disabledCause: string): void {
592
+ this.#removeCredentialById(id);
593
+ // Fire-and-forget: tell the broker to persist the disable.
594
+ this.#client.disableCredential(id, disabledCause).catch(error => {
595
+ logger.warn("auth-broker disable propagation failed", { id, error: String(error) });
596
+ });
597
+ }
598
+
599
+ async deleteAuthCredentialRemote(id: number, disabledCause: string): Promise<boolean> {
600
+ const found = this.#snapshot.credentials.some(entry => entry.id === id);
601
+ if (!found) return false;
602
+ await this.#client.disableCredential(id, disabledCause);
603
+ this.#removeCredentialById(id);
604
+ this.#maybeRefreshSnapshot("delete credential");
605
+ return true;
606
+ }
607
+
608
+ tryDisableAuthCredentialIfMatches(id: number, _expectedData: string, disabledCause: string): boolean {
609
+ const found = this.#snapshot.credentials.find(entry => entry.id === id);
610
+ if (!found) return false;
611
+ this.deleteAuthCredential(id, disabledCause);
612
+ return true;
613
+ }
614
+
615
+ async waitForFreshSnapshot(maxWaitMs: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
616
+ const previousGeneration = this.#generation;
617
+ const result = await this.#client.fetchSnapshot({
618
+ ifGenerationGt: this.#generation,
619
+ waitMs: maxWaitMs,
620
+ signal: opts.signal,
621
+ });
622
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
623
+ return this.#generation !== previousGeneration;
624
+ }
625
+
626
+ async prepareForRequest(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
627
+ const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
628
+ if (entry?.credential.type !== "oauth" || entry.rotatesInMs === null) return false;
629
+ const remainingMs = this.#snapshotReceivedAt + entry.rotatesInMs - Date.now();
630
+ if (remainingMs > WAIT_THRESHOLD_MS) return false;
631
+ return this.waitForFreshSnapshot(MAX_WAIT_MS, opts);
632
+ }
633
+
634
+ async markCredentialSuspect(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<void> {
635
+ const { entry } = await this.#client.refreshCredential(credentialId, opts.signal);
636
+ if (entry.credential.type !== "oauth") {
637
+ throw new AIError.AuthBrokerError(`Broker returned non-OAuth credential for id=${credentialId}`);
638
+ }
639
+ if (!this.#applyCredentialEntry(entry)) {
640
+ throw new AIError.AuthBrokerError(
641
+ `Broker refreshed credential id=${credentialId} outside the configured account pool`,
642
+ );
643
+ }
644
+ this.#maybeRefreshSnapshot("suspect credential refresh");
645
+ }
646
+
647
+ replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[] {
648
+ throw new AIError.AuthBrokerError(
649
+ "RemoteAuthCredentialStore is read-only on the client. Use `omp auth-broker login <provider>` to mutate credentials.",
650
+ );
651
+ }
652
+
653
+ upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[] {
654
+ throw new AIError.AuthBrokerError(
655
+ "RemoteAuthCredentialStore is read-only on the client. Use `omp auth-broker login <provider>` to mutate credentials.",
656
+ );
657
+ }
658
+
659
+ deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void {
660
+ throw new AIError.AuthBrokerError(
661
+ "RemoteAuthCredentialStore is read-only on the client. Use `omp auth-broker logout <provider>` to mutate credentials.",
662
+ );
663
+ }
664
+
665
+ /**
666
+ * Upsert a single credential through the broker. The broker server is the
667
+ * canonical writer — see `POST /v1/credential`. The redacted snapshot
668
+ * entries returned by the server replace the provider's rows in our local
669
+ * snapshot, and the global snapshot is then refreshed in the background so
670
+ * any concurrent peer (refresh, generation bump) stays in sync.
671
+ */
672
+ async upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]> {
673
+ const { entries } = await this.#client.uploadCredential(provider, credential);
674
+ this.#applyProviderEntries(provider, entries);
675
+ this.#maybeRefreshSnapshot("upload");
676
+ return this.listAuthCredentials(provider);
677
+ }
678
+
679
+ /**
680
+ * Replace-all semantics: disable every active credential for the provider,
681
+ * then upload each of the new credentials. Used by API-key login so a new
682
+ * key clobbers any previously stored key for the same provider.
683
+ */
684
+ async replaceAuthCredentialsRemote(
685
+ provider: string,
686
+ credentials: AuthCredential[],
687
+ ): Promise<StoredAuthCredential[]> {
688
+ const existing = this.listAuthCredentials(provider);
689
+ for (const entry of existing) {
690
+ try {
691
+ await this.#client.disableCredential(entry.id, "replaced by newer credential");
692
+ } catch (error) {
693
+ logger.warn("auth-broker disable during replace failed", {
694
+ provider,
695
+ id: entry.id,
696
+ error: String(error),
697
+ });
698
+ }
699
+ }
700
+ // Snapshot reflects the disables before we add the new rows so a concurrent
701
+ // reader cannot momentarily see old + new together for the same provider.
702
+ this.#removeProviderEntries(provider);
703
+ for (const credential of credentials) {
704
+ const { entries } = await this.#client.uploadCredential(provider, credential);
705
+ this.#applyProviderEntries(provider, entries);
706
+ }
707
+ this.#maybeRefreshSnapshot("replace");
708
+ return this.listAuthCredentials(provider);
709
+ }
710
+
711
+ /**
712
+ * Logout: disable every active credential for the provider on the broker,
713
+ * then drop them from the local snapshot. Refresh fetches the authoritative
714
+ * post-state in the background.
715
+ */
716
+ async deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void> {
717
+ const existing = this.listAuthCredentials(provider);
718
+ for (const entry of existing) {
719
+ try {
720
+ await this.#client.disableCredential(entry.id, disabledCause);
721
+ } catch (error) {
722
+ logger.warn("auth-broker disable during delete failed", {
723
+ provider,
724
+ id: entry.id,
725
+ error: String(error),
726
+ });
727
+ }
728
+ }
729
+ this.#removeProviderEntries(provider);
730
+ this.#maybeRefreshSnapshot("delete");
731
+ }
732
+
733
+ #applyProviderEntries(provider: string, entries: AuthCredentialSnapshotEntry[]): void {
734
+ // `entries` is the broker's authoritative post-upsert list of rows for
735
+ // `provider`. Drop our existing rows for the same provider and splice in
736
+ // the fresh set — preserving every other provider's rows in place.
737
+ const existingBlocks = new Map(
738
+ this.#snapshot.credentials
739
+ .filter(entry => entry.provider === provider && entry.blocks !== undefined)
740
+ .map(entry => [entry.id, entry.blocks] as const),
741
+ );
742
+ const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
743
+ const incoming = entries
744
+ .filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
745
+ .map(entry => credentialEntryWithBlocks(entry, existingBlocks.get(entry.id)));
746
+ this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
747
+ }
748
+ #applyCredentialEntry(entry: AuthCredentialSnapshotEntry): boolean {
749
+ if (!isCredentialInAccountPool(entry, this.#accountPool)) {
750
+ this.#removeCredentialById(entry.id);
751
+ return false;
752
+ }
753
+ const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
754
+ const existingBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
755
+ const incoming = credentialEntryWithBlocks(entry, existingBlocks);
756
+ if (index === -1) {
757
+ this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
758
+ return true;
759
+ }
760
+ const credentials = [...this.#snapshot.credentials];
761
+ credentials[index] = incoming;
762
+ this.#snapshot = { ...this.#snapshot, credentials };
763
+ return true;
764
+ }
765
+
766
+ #removeProviderEntries(provider: string): void {
767
+ const next = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
768
+ this.#snapshot = { ...this.#snapshot, credentials: next };
769
+ }
770
+
771
+ #removeCredentialById(id: number): void {
772
+ const next = this.#snapshot.credentials.filter(entry => entry.id !== id);
773
+ this.#snapshot = { ...this.#snapshot, credentials: next };
774
+ }
775
+
776
+ #normalizeSnapshotEntryBlocks(entry: SnapshotEntry, nowMs: number): SnapshotEntry {
777
+ if (!entry.blocks || entry.blocks.length === 0) return entry;
778
+ const blocks = entry.blocks
779
+ .filter(block => block.blockedUntilMs > nowMs)
780
+ .map(block => ({
781
+ providerKey: block.providerKey,
782
+ blockScope: block.blockScope,
783
+ blockedUntilMs: block.blockedUntilMs,
784
+ ...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
785
+ }))
786
+ .sort(compareCredentialBlockSnapshots);
787
+ if (blocks.length > 0) return { ...entry, blocks };
788
+ const next: SnapshotEntry = { ...entry };
789
+ delete next.blocks;
790
+ return next;
791
+ }
792
+
793
+ #upsertSnapshotBlock(block: StoredCredentialBlock): void {
794
+ const index = this.#snapshot.credentials.findIndex(entry => entry.id === block.credentialId);
795
+ if (index === -1) return;
796
+ const entry = this.#snapshot.credentials[index]!;
797
+ const incoming = toCredentialBlockSnapshot(block);
798
+ const blocks = entry.blocks ? [...entry.blocks] : [];
799
+ const blockIndex = blocks.findIndex(
800
+ candidate => candidate.providerKey === incoming.providerKey && candidate.blockScope === incoming.blockScope,
801
+ );
802
+ if (blockIndex === -1) {
803
+ blocks.push(incoming);
804
+ } else {
805
+ const existing = blocks[blockIndex]!;
806
+ blocks[blockIndex] = {
807
+ ...existing,
808
+ blockedUntilMs: Math.max(existing.blockedUntilMs, incoming.blockedUntilMs),
809
+ };
810
+ }
811
+ blocks.sort(compareCredentialBlockSnapshots);
812
+ const credentials = [...this.#snapshot.credentials];
813
+ credentials[index] = { ...entry, blocks };
814
+ this.#snapshot = { ...this.#snapshot, credentials };
815
+ }
816
+
817
+ #deleteSnapshotBlocks(credentialId: number): void {
818
+ const index = this.#snapshot.credentials.findIndex(entry => entry.id === credentialId);
819
+ if (index === -1) return;
820
+ const entry = this.#snapshot.credentials[index]!;
821
+ if (!entry.blocks || entry.blocks.length === 0) return;
822
+ const next: SnapshotEntry = { ...entry };
823
+ delete next.blocks;
824
+ const credentials = [...this.#snapshot.credentials];
825
+ credentials[index] = next;
826
+ this.#snapshot = { ...this.#snapshot, credentials };
827
+ }
828
+
829
+ #pruneExpiredCredentialBlocks(nowMs: number): void {
830
+ let changed = false;
831
+ const credentials = this.#snapshot.credentials.map(entry => {
832
+ if (!entry.blocks || entry.blocks.length === 0) return entry;
833
+ const blocks = entry.blocks.filter(block => block.blockedUntilMs > nowMs);
834
+ if (blocks.length === entry.blocks.length) return entry;
835
+ changed = true;
836
+ if (blocks.length > 0) return { ...entry, blocks };
837
+ const next: SnapshotEntry = { ...entry };
838
+ delete next.blocks;
839
+ return next;
840
+ });
841
+ if (changed) this.#snapshot = { ...this.#snapshot, credentials };
842
+ }
843
+
844
+ /**
845
+ * Fire-and-forget `refreshSnapshot()` after a write. When the SSE stream is
846
+ * active the broker will deliver the new generation push, so the extra GET
847
+ * is wasted bandwidth and we skip it.
848
+ */
849
+ #maybeRefreshSnapshot(reason: string): void {
850
+ if (this.#streamingActive) return;
851
+ void this.refreshSnapshot().catch(error => {
852
+ logger.debug("auth-broker snapshot refresh after write failed", { reason, error: String(error) });
853
+ });
854
+ }
855
+
856
+ getCache(key: string): string | null {
857
+ const entry = this.#cache.get(key);
858
+ if (!entry) return null;
859
+ if (entry.expiresAtSec * 1000 <= Date.now()) {
860
+ this.#cache.delete(key);
861
+ return null;
862
+ }
863
+ return entry.value;
864
+ }
865
+
866
+ setCache(key: string, value: string, expiresAtSec: number): void {
867
+ this.#cache.set(key, { value, expiresAtSec });
868
+ }
869
+
870
+ /** Drop all cache rows whose keys start with the supplied prefix. */
871
+ deleteCachePrefix(prefix: string): void {
872
+ for (const key of this.#cache.keys()) {
873
+ if (key.startsWith(prefix)) this.#cache.delete(key);
874
+ }
875
+ }
876
+
877
+ cleanExpiredCache(): void {
878
+ const nowSec = Math.floor(Date.now() / 1000);
879
+ for (const [key, entry] of this.#cache) {
880
+ if (entry.expiresAtSec <= nowSec) this.#cache.delete(key);
881
+ }
882
+ }
883
+
884
+ async invalidateUsageCache(signal?: AbortSignal): Promise<void> {
885
+ this.#invalidateUsageCache();
886
+ await this.#client.notifyUsageStale(signal).catch(err => {
887
+ logger.warn("auth-broker notification of stale usage failed", { error: String(err) });
888
+ });
889
+ }
890
+
891
+ #invalidateUsageCache(): void {
892
+ this.#usageCache = undefined;
893
+ this.#usageInflight = undefined;
894
+ this.#usageCacheEpoch += 1;
895
+ }
896
+
897
+ /**
898
+ * Store-level hook consumed by `AuthStorage` — routes refresh through the
899
+ * broker so the actual refresh token never leaves the broker host. Returns
900
+ * the broker-redacted credential with {@link REMOTE_REFRESH_SENTINEL} in
901
+ * the `refresh` slot.
902
+ */
903
+ async refreshOAuthCredential(
904
+ _provider: Provider,
905
+ credentialId: number,
906
+ _credential: OAuthCredential,
907
+ signal?: AbortSignal,
908
+ ): Promise<OAuthCredentials> {
909
+ const { entry } = await this.#client.refreshCredential(credentialId, signal);
910
+ if (entry.credential.type !== "oauth") {
911
+ throw new AIError.AuthBrokerError(`Broker returned non-OAuth credential for id=${credentialId}`);
912
+ }
913
+ if (!this.#applyCredentialEntry(entry)) {
914
+ throw new AIError.AuthBrokerError(
915
+ `Broker refreshed credential id=${credentialId} outside the configured account pool`,
916
+ );
917
+ }
918
+ if (!this.#streamingActive) {
919
+ await this.refreshSnapshot().catch(error => {
920
+ logger.debug("auth-broker snapshot refresh after credential refresh failed", { error: String(error) });
921
+ });
922
+ }
923
+ const refreshed = entry.credential;
924
+ return {
925
+ access: refreshed.access,
926
+ refresh: REMOTE_REFRESH_SENTINEL,
927
+ expires: refreshed.expires,
928
+ accountId: refreshed.accountId,
929
+ email: refreshed.email,
930
+ projectId: refreshed.projectId,
931
+ enterpriseUrl: refreshed.enterpriseUrl,
932
+ };
933
+ }
934
+
935
+ /**
936
+ * Store-level hook consumed by `AuthStorage.fetchUsageReports()` — proxies
937
+ * to the broker's `/v1/usage` endpoint. The broker's egress IP isn't
938
+ * rate-limited by Anthropic's per-IP `/usage` cap the way a heavy
939
+ * residential laptop is, so all credentials surface every cycle.
940
+ */
941
+ async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
942
+ const reports = await this.#raceWithSignal(this.#loadUsageReports(), signal);
943
+ if (!reports) return null;
944
+ return this.#filterUsageReports(this.#applyUsageOverlays(reports));
945
+ }
946
+
947
+ /**
948
+ * Per-credential usage hook consumed by `AuthStorage.#getUsageReport`. Pulls
949
+ * the aggregate broker `/v1/usage` once and serves all callers from the
950
+ * same response (coalesced + cached), then overlays any client-observed
951
+ * header hints for the matching credential.
952
+ *
953
+ * The broker already aggregates with its own 30s TTL on the server side; our
954
+ * 15s client TTL is below that so we usually re-use the broker's cache too.
955
+ */
956
+ async getUsageReport(
957
+ provider: Provider,
958
+ credential: OAuthCredential,
959
+ signal?: AbortSignal,
960
+ ): Promise<UsageReport | null> {
961
+ const reports = await this.#raceWithSignal(this.#loadUsageReports(), signal);
962
+ const visibleReports = reports ? this.#filterUsageReports(reports) : null;
963
+ const matched = visibleReports ? matchUsageReport(visibleReports, provider, credential) : null;
964
+ const overlay = this.#getActiveUsageOverlay(provider, credential);
965
+ if (matched && overlay) return mergeUsageReports(matched, overlay);
966
+ return overlay ?? matched;
967
+ }
968
+
969
+ /**
970
+ * Hot path — called per `getUsageReport()`/`fetchUsageReports()` (status-line
971
+ * refresh cadence). The oauth-credential lookup is memoized on `#snapshot`
972
+ * identity (every update site replaces the reference), and the filtered
973
+ * output on (reports identity, lookup identity) — `#loadUsageReports`
974
+ * serves the same array for 15s, so steady-state calls are O(1).
975
+ */
976
+ #filterUsageReports(reports: UsageReport[]): UsageReport[] {
977
+ const accountPool = this.#accountPool;
978
+ if (!accountPool) return reports;
979
+ let lookup = this.#usageFilterLookup;
980
+ if (!lookup || lookup.snapshot !== this.#snapshot) {
981
+ const byProvider = new Map<Provider, OAuthCredential[]>();
982
+ for (const entry of this.#snapshot.credentials) {
983
+ if (entry.credential.type !== "oauth") continue;
984
+ const list = byProvider.get(entry.provider);
985
+ if (list) list.push(entry.credential);
986
+ else byProvider.set(entry.provider, [entry.credential]);
987
+ }
988
+ lookup = { snapshot: this.#snapshot, byProvider };
989
+ this.#usageFilterLookup = lookup;
990
+ }
991
+ const memo = this.#usageFilterResult;
992
+ if (memo && memo.input === reports && memo.byProvider === lookup.byProvider) return memo.output;
993
+ const byProvider = lookup.byProvider;
994
+ const output = reports.filter(report => {
995
+ if (!accountPool.has(report.provider)) return true;
996
+ const credentials = byProvider.get(report.provider);
997
+ if (!credentials) return false;
998
+ return credentials.some(credential => usageReportMatchesCredential(report, credential));
999
+ });
1000
+ this.#usageFilterResult = { input: reports, byProvider, output };
1001
+ return output;
1002
+ }
1003
+
1004
+ ingestUsageReport(provider: Provider, credential: OAuthCredential, report: UsageReport): boolean {
1005
+ const key = usageOverlayKey(provider, credential);
1006
+ if (!key) return false;
1007
+ const activeOverlay = this.#getActiveUsageOverlay(provider, credential);
1008
+ this.#usageOverlays.set(key, activeOverlay ? mergeUsageReports(activeOverlay, report) : report);
1009
+ return true;
1010
+ }
1011
+
1012
+ #getActiveUsageOverlay(provider: Provider, credential: OAuthCredential): UsageReport | undefined {
1013
+ const key = usageOverlayKey(provider, credential);
1014
+ if (!key) return undefined;
1015
+ const overlay = this.#usageOverlays.get(key);
1016
+ if (!overlay) return undefined;
1017
+ if (Date.now() - overlay.fetchedAt >= USAGE_CACHE_TTL_MS) {
1018
+ this.#usageOverlays.delete(key);
1019
+ return undefined;
1020
+ }
1021
+ return overlay;
1022
+ }
1023
+
1024
+ #applyUsageOverlays(reports: UsageReport[]): UsageReport[] {
1025
+ const overlays = [...this.#usageOverlays.values()].filter(
1026
+ overlay => Date.now() - overlay.fetchedAt < USAGE_CACHE_TTL_MS,
1027
+ );
1028
+ if (overlays.length === 0) return reports;
1029
+ const merged = [...reports];
1030
+ for (const overlay of overlays) {
1031
+ const matchIndex = findMatchingReportIndex(merged, overlay);
1032
+ if (matchIndex === -1) {
1033
+ merged.push(overlay);
1034
+ } else {
1035
+ merged[matchIndex] = mergeUsageReports(merged[matchIndex]!, overlay);
1036
+ }
1037
+ }
1038
+ return merged;
1039
+ }
1040
+
1041
+ /**
1042
+ * Reject the awaited promise when the caller's signal aborts, without
1043
+ * affecting the shared upstream fetch. Used to give each caller their
1044
+ * own cancel without one caller's abort cascading into a peer's in-flight
1045
+ * request through the single-flight `#usageInflight`.
1046
+ */
1047
+ #raceWithSignal<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
1048
+ if (!signal) return promise;
1049
+ if (signal.aborted) return Promise.reject(new AIError.AbortError("auth-broker request aborted"));
1050
+ return new Promise<T>((resolve, reject) => {
1051
+ const onAbort = (): void => {
1052
+ signal.removeEventListener("abort", onAbort);
1053
+ reject(new AIError.AbortError("auth-broker request aborted"));
1054
+ };
1055
+ signal.addEventListener("abort", onAbort, { once: true });
1056
+ promise.then(
1057
+ value => {
1058
+ signal.removeEventListener("abort", onAbort);
1059
+ resolve(value);
1060
+ },
1061
+ err => {
1062
+ signal.removeEventListener("abort", onAbort);
1063
+ reject(err);
1064
+ },
1065
+ );
1066
+ });
1067
+ }
1068
+
1069
+ #loadUsageReports(): Promise<UsageReport[] | null> {
1070
+ const cached = this.#usageCache;
1071
+ if (cached && Date.now() - cached.fetchedAt < USAGE_CACHE_TTL_MS) {
1072
+ return Promise.resolve(cached.reports);
1073
+ }
1074
+ if (this.#usageInflight) return this.#usageInflight;
1075
+ const epoch = this.#usageCacheEpoch;
1076
+ const inflight = this.#client
1077
+ .fetchUsage()
1078
+ .then(body => {
1079
+ if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
1080
+ this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
1081
+ return body.reports;
1082
+ })
1083
+ .catch(error => {
1084
+ logger.warn("auth-broker usage fetch failed", { error: String(error) });
1085
+ // Documented 15s TTL fallback: cache the null so sequential callers
1086
+ // don't re-hit the broker while it's still down. See
1087
+ // docs/auth-broker-gateway.md § "Client-side single-flight".
1088
+ if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
1089
+ this.#usageCache = { reports: null, fetchedAt: Date.now() };
1090
+ return null;
1091
+ })
1092
+ .finally(() => {
1093
+ if (this.#usageInflight === inflight) this.#usageInflight = undefined;
1094
+ });
1095
+ this.#usageInflight = inflight;
1096
+ return inflight;
1097
+ }
1098
+
1099
+ /**
1100
+ * Fold locally observed request usage into the pending report and schedule
1101
+ * a flush. One `POST /v1/usage/observed` at most per flush interval; on
1102
+ * failure the batch is retained and retried with the next flush. A 404
1103
+ * (pre-endpoint broker) disables reporting for the life of this store.
1104
+ */
1105
+ recordObservedUsage(entries: ObservedUsageEntry[]): void {
1106
+ if (this.#closed || this.#observedUsageUnsupported) return;
1107
+ for (const entry of entries) {
1108
+ const key = `${entry.provider}\u0000${entry.model}`;
1109
+ const pending = this.#observedUsage.get(key);
1110
+ if (pending) {
1111
+ pending.at = Math.max(pending.at, entry.at);
1112
+ pending.requests += entry.requests;
1113
+ pending.inputTokens += entry.inputTokens;
1114
+ pending.outputTokens += entry.outputTokens;
1115
+ pending.cacheReadTokens += entry.cacheReadTokens;
1116
+ pending.cacheWriteTokens += entry.cacheWriteTokens;
1117
+ pending.costUsd += entry.costUsd;
1118
+ } else {
1119
+ this.#observedUsage.set(key, { ...entry });
1120
+ }
1121
+ }
1122
+ if (this.#observedUsage.size > 0 && this.#observedUsageTimer === undefined) {
1123
+ this.#observedUsageTimer = setTimeout(() => {
1124
+ this.#observedUsageTimer = undefined;
1125
+ void this.#flushObservedUsage();
1126
+ }, this.#observedUsageFlushMs);
1127
+ this.#observedUsageTimer.unref?.();
1128
+ }
1129
+ }
1130
+
1131
+ async #flushObservedUsage(): Promise<void> {
1132
+ if (this.#observedUsage.size === 0 || this.#observedUsageUnsupported) return;
1133
+ const batch = [...this.#observedUsage.values()];
1134
+ this.#observedUsage.clear();
1135
+ try {
1136
+ await this.#client.reportClientUsage({
1137
+ installId: getInstallId(),
1138
+ hostname: os.hostname(),
1139
+ entries: batch,
1140
+ });
1141
+ } catch (error) {
1142
+ const status = error instanceof AuthBrokerError ? error.status : undefined;
1143
+ if (status === 404 || status === 501) {
1144
+ // Broker predates the endpoint (or store can't persist) — stop trying.
1145
+ this.#observedUsageUnsupported = true;
1146
+ logger.debug("auth-broker does not accept observed usage; reporting disabled", { status });
1147
+ return;
1148
+ }
1149
+ logger.debug("auth-broker observed usage flush failed; retrying next flush", { error: String(error) });
1150
+ // Merge the failed batch back under the (possibly refilled) buffer so
1151
+ // nothing is lost; bounded because entries are keyed per (provider, model).
1152
+ if (!this.#closed) this.recordObservedUsage(batch);
1153
+ }
1154
+ }
1155
+
1156
+ close(): void {
1157
+ if (this.#closed) return;
1158
+ this.#closed = true;
1159
+ this.#backgroundAbort.abort();
1160
+ if (this.#observedUsageTimer !== undefined) {
1161
+ clearTimeout(this.#observedUsageTimer);
1162
+ this.#observedUsageTimer = undefined;
1163
+ }
1164
+ // Best-effort final flush; failures are dropped (the process is exiting).
1165
+ if (this.#observedUsage.size > 0) void this.#flushObservedUsage();
1166
+ this.#cache.clear();
1167
+ this.#usageOverlays.clear();
1168
+ }
1169
+ }
1170
+
1171
+ /**
1172
+ * Match a broker-supplied usage report to a specific OAuth credential. The
1173
+ * broker returns aggregate reports across all credentials it manages, so we
1174
+ * pick the one whose identity (accountId / email / projectId) lines up with
1175
+ * the credential the caller is asking about.
1176
+ *
1177
+ * Falls back to the lone candidate when only one matches the provider; falls
1178
+ * through to `null` when nothing matches, which `AuthStorage` treats as "no
1179
+ * usage data" (ranking proceeds without a usage signal for this credential).
1180
+ */
1181
+ function matchUsageReport(reports: UsageReport[], provider: Provider, credential: OAuthCredential): UsageReport | null {
1182
+ const all = reports.filter(report => report.provider === provider);
1183
+ if (all.length === 0) return null;
1184
+ // Org precedence, decisive on EITHER side: an org-scoped credential may
1185
+ // only take its own org's report, and an org-less (legacy) credential may
1186
+ // only take org-less reports — the shared email/account would otherwise
1187
+ // hand one subscription the OTHER subscription's pool (e.g. mark healthy
1188
+ // Max exhausted via Team's report, or rank a legacy row on a sibling's
1189
+ // numbers).
1190
+ const orgId = credential.orgId?.trim().toLowerCase();
1191
+ const accountId = credential.accountId?.trim().toLowerCase();
1192
+ const email = credential.email?.trim().toLowerCase();
1193
+ const projectId = credential.projectId?.trim().toLowerCase();
1194
+ if (orgId) {
1195
+ const sameOrg: UsageReport[] = [];
1196
+ let sawReportOrg = false;
1197
+ for (const report of all) {
1198
+ const metaOrg = readMetadataString((report.metadata ?? {}) as Record<string, unknown>, "orgId");
1199
+ if (metaOrg) {
1200
+ sawReportOrg = true;
1201
+ if (metaOrg.toLowerCase() === orgId) sameOrg.push(report);
1202
+ }
1203
+ }
1204
+ // Org-attributed reports exist: the shared org is a GATE, not a match.
1205
+ // Two Team members share the org id while drawing on per-user pools,
1206
+ // so the credential's own base identity must still line up inside the
1207
+ // same-org subset — a lone sibling report is NOT ours. An org-only
1208
+ // credential (no base identifiers) takes the lone same-org report and
1209
+ // treats several as ambiguous. None in our org → "no usage data"
1210
+ // rather than mis-attributing another org's pool.
1211
+ if (sawReportOrg) {
1212
+ if (accountId || email || projectId) {
1213
+ for (const report of sameOrg) {
1214
+ if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
1215
+ }
1216
+ return null;
1217
+ }
1218
+ return sameOrg.length === 1 ? sameOrg[0]! : null;
1219
+ }
1220
+ // No surviving report carries an org at all: presence mismatch is a
1221
+ // non-match too — the sole org-less report may be a legacy sibling
1222
+ // row's pool, and handing it to a scoped credential would rank/block
1223
+ // on the wrong quota. "No usage data" degrades gracefully instead.
1224
+ return null;
1225
+ }
1226
+ const candidates = all.filter(
1227
+ report => !readMetadataString((report.metadata ?? {}) as Record<string, unknown>, "orgId"),
1228
+ );
1229
+ if (candidates.length === 0) return null;
1230
+ if (all.length === 1 && candidates.length === 1) return candidates[0];
1231
+ for (const report of candidates) {
1232
+ if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
1233
+ }
1234
+ return null;
1235
+ }
1236
+
1237
+ function usageReportMatchesCredential(report: UsageReport, credential: OAuthCredential): boolean {
1238
+ const metadata = (report.metadata ?? {}) as Record<string, unknown>;
1239
+ const credentialOrg = credential.orgId?.trim().toLowerCase();
1240
+ const reportOrg = readMetadataString(metadata, "orgId")?.toLowerCase();
1241
+ if (credentialOrg !== reportOrg) return false;
1242
+
1243
+ const accountId = credential.accountId?.trim().toLowerCase();
1244
+ const email = credential.email?.trim().toLowerCase();
1245
+ const projectId = credential.projectId?.trim().toLowerCase();
1246
+ if (accountId || email || projectId) {
1247
+ return reportMatchesIdentity(report, accountId, email, projectId);
1248
+ }
1249
+ return credentialOrg !== undefined;
1250
+ }
1251
+
1252
+ function findMatchingReportIndex(reports: UsageReport[], overlay: UsageReport): number {
1253
+ const all = reports
1254
+ .map((report, index) => ({ report, index }))
1255
+ .filter(candidate => candidate.report.provider === overlay.provider);
1256
+ if (all.length === 0) return -1;
1257
+ const metadata = (overlay.metadata ?? {}) as Record<string, unknown>;
1258
+ // Org precedence — mirror matchUsageReport: an org-attributed overlay may
1259
+ // only merge into a report of the SAME org, and an org-less overlay may
1260
+ // only merge into an org-less report. Within the same org the overlay's
1261
+ // base identity must still match — two Team members' reports share the
1262
+ // org id but must not swallow each other's header ingests.
1263
+ const overlayOrg = readMetadataString(metadata, "orgId")?.toLowerCase();
1264
+ const accountId = readMetadataString(metadata, "accountId")?.toLowerCase();
1265
+ const email = readMetadataString(metadata, "email")?.toLowerCase();
1266
+ const projectId = readMetadataString(metadata, "projectId")?.toLowerCase();
1267
+ if (overlayOrg) {
1268
+ const sameOrg: { report: UsageReport; index: number }[] = [];
1269
+ let sawReportOrg = false;
1270
+ for (const candidate of all) {
1271
+ const candidateOrg = readMetadataString((candidate.report.metadata ?? {}) as Record<string, unknown>, "orgId");
1272
+ if (candidateOrg) {
1273
+ sawReportOrg = true;
1274
+ if (candidateOrg.toLowerCase() === overlayOrg) sameOrg.push(candidate);
1275
+ }
1276
+ }
1277
+ if (sawReportOrg) {
1278
+ if (accountId || email || projectId) {
1279
+ for (const candidate of sameOrg) {
1280
+ if (reportMatchesIdentity(candidate.report, accountId, email, projectId)) return candidate.index;
1281
+ }
1282
+ return -1;
1283
+ }
1284
+ return sameOrg.length === 1 ? sameOrg[0]!.index : -1;
1285
+ }
1286
+ // Presence mismatch — mirror matchUsageReport: an org-scoped overlay
1287
+ // never merges into an org-less report; it becomes its own report row.
1288
+ return -1;
1289
+ }
1290
+ const candidates = all.filter(
1291
+ candidate => !readMetadataString((candidate.report.metadata ?? {}) as Record<string, unknown>, "orgId"),
1292
+ );
1293
+ if (candidates.length === 0) return -1;
1294
+ if (all.length === 1 && candidates.length === 1) return candidates[0]!.index;
1295
+ for (const candidate of candidates) {
1296
+ if (reportMatchesIdentity(candidate.report, accountId, email, projectId)) return candidate.index;
1297
+ }
1298
+ return -1;
1299
+ }
1300
+
1301
+ function reportMatchesIdentity(
1302
+ report: UsageReport,
1303
+ accountId: string | undefined,
1304
+ email: string | undefined,
1305
+ projectId: string | undefined,
1306
+ ): boolean {
1307
+ const metadata = (report.metadata ?? {}) as Record<string, unknown>;
1308
+ if (accountId) {
1309
+ const metaAccount = readMetadataString(metadata, "accountId") ?? readMetadataString(metadata, "account_id");
1310
+ if (metaAccount && metaAccount.toLowerCase() === accountId) return true;
1311
+ for (const limit of report.limits) {
1312
+ if (limit.scope.accountId?.toLowerCase() === accountId) return true;
1313
+ }
1314
+ }
1315
+ if (email) {
1316
+ const metaEmail = readMetadataString(metadata, "email");
1317
+ if (metaEmail && metaEmail.toLowerCase() === email) return true;
1318
+ }
1319
+ if (projectId) {
1320
+ const metaProject = readMetadataString(metadata, "projectId") ?? readMetadataString(metadata, "project_id");
1321
+ if (metaProject && metaProject.toLowerCase() === projectId) return true;
1322
+ for (const limit of report.limits) {
1323
+ if (limit.scope.projectId?.toLowerCase() === projectId) return true;
1324
+ }
1325
+ }
1326
+ return false;
1327
+ }
1328
+
1329
+ function readMetadataString(metadata: Record<string, unknown>, key: string): string | undefined {
1330
+ const value = metadata[key];
1331
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1332
+ }