open-multi-agent-kit 0.96.0 → 0.96.2

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 (351) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +34 -6
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +15 -20
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/resource-doctor-cli.d.ts +22 -0
  10. package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
  11. package/dist/commands/resource-doctor-cli.js +98 -0
  12. package/dist/commands/resource-doctor-cli.js.map +1 -0
  13. package/dist/commands/sdk-session-cli.d.ts +24 -0
  14. package/dist/commands/sdk-session-cli.d.ts.map +1 -0
  15. package/dist/commands/sdk-session-cli.js +225 -0
  16. package/dist/commands/sdk-session-cli.js.map +1 -0
  17. package/dist/core/advisory-judge-model.d.ts +18 -0
  18. package/dist/core/advisory-judge-model.d.ts.map +1 -0
  19. package/dist/core/advisory-judge-model.js +168 -0
  20. package/dist/core/advisory-judge-model.js.map +1 -0
  21. package/dist/core/advisory-judge-response.d.ts +8 -0
  22. package/dist/core/advisory-judge-response.d.ts.map +1 -0
  23. package/dist/core/advisory-judge-response.js +61 -0
  24. package/dist/core/advisory-judge-response.js.map +1 -0
  25. package/dist/core/advisory-judge-types.d.ts +53 -0
  26. package/dist/core/advisory-judge-types.d.ts.map +1 -0
  27. package/dist/core/advisory-judge-types.js +2 -0
  28. package/dist/core/advisory-judge-types.js.map +1 -0
  29. package/dist/core/advisory-judge.d.ts +8 -0
  30. package/dist/core/advisory-judge.d.ts.map +1 -0
  31. package/dist/core/advisory-judge.js +178 -0
  32. package/dist/core/advisory-judge.js.map +1 -0
  33. package/dist/core/agent-session-services.d.ts +1 -0
  34. package/dist/core/agent-session-services.d.ts.map +1 -1
  35. package/dist/core/agent-session-services.js +1 -0
  36. package/dist/core/agent-session-services.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +41 -2
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +396 -179
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/auth-storage.d.ts.map +1 -1
  42. package/dist/core/auth-storage.js +28 -4
  43. package/dist/core/auth-storage.js.map +1 -1
  44. package/dist/core/compaction-gate.d.ts +28 -0
  45. package/dist/core/compaction-gate.d.ts.map +1 -0
  46. package/dist/core/compaction-gate.js +34 -0
  47. package/dist/core/compaction-gate.js.map +1 -0
  48. package/dist/core/completion-sound.d.ts +109 -0
  49. package/dist/core/completion-sound.d.ts.map +1 -0
  50. package/dist/core/completion-sound.js +165 -0
  51. package/dist/core/completion-sound.js.map +1 -0
  52. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  53. package/dist/core/context-budget-system-prompt-items.js +5 -3
  54. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  55. package/dist/core/domain-loadouts.d.ts.map +1 -1
  56. package/dist/core/domain-loadouts.js +2 -2
  57. package/dist/core/domain-loadouts.js.map +1 -1
  58. package/dist/core/durable-goal-checkpoint.d.ts +6 -0
  59. package/dist/core/durable-goal-checkpoint.d.ts.map +1 -0
  60. package/dist/core/durable-goal-checkpoint.js +195 -0
  61. package/dist/core/durable-goal-checkpoint.js.map +1 -0
  62. package/dist/core/durable-goal-parse.d.ts +3 -0
  63. package/dist/core/durable-goal-parse.d.ts.map +1 -0
  64. package/dist/core/durable-goal-parse.js +86 -0
  65. package/dist/core/durable-goal-parse.js.map +1 -0
  66. package/dist/core/durable-goal-reducer.d.ts +3 -0
  67. package/dist/core/durable-goal-reducer.d.ts.map +1 -0
  68. package/dist/core/durable-goal-reducer.js +175 -0
  69. package/dist/core/durable-goal-reducer.js.map +1 -0
  70. package/dist/core/durable-goal-store.d.ts +19 -0
  71. package/dist/core/durable-goal-store.d.ts.map +1 -0
  72. package/dist/core/durable-goal-store.js +126 -0
  73. package/dist/core/durable-goal-store.js.map +1 -0
  74. package/dist/core/durable-goal.d.ts +92 -0
  75. package/dist/core/durable-goal.d.ts.map +1 -0
  76. package/dist/core/durable-goal.js +62 -0
  77. package/dist/core/durable-goal.js.map +1 -0
  78. package/dist/core/extensions/builtin/goal-controller.d.ts +3 -0
  79. package/dist/core/extensions/builtin/goal-controller.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/goal-controller.js +125 -0
  81. package/dist/core/extensions/builtin/goal-controller.js.map +1 -0
  82. package/dist/core/extensions/builtin/identical-loop.d.ts +3 -0
  83. package/dist/core/extensions/builtin/identical-loop.d.ts.map +1 -0
  84. package/dist/core/extensions/builtin/identical-loop.js +31 -0
  85. package/dist/core/extensions/builtin/identical-loop.js.map +1 -0
  86. package/dist/core/extensions/builtin/prompt-preset.d.ts +3 -0
  87. package/dist/core/extensions/builtin/prompt-preset.d.ts.map +1 -0
  88. package/dist/core/extensions/builtin/prompt-preset.js +13 -0
  89. package/dist/core/extensions/builtin/prompt-preset.js.map +1 -0
  90. package/dist/core/extensions/builtin/tool-pair-repair.d.ts +3 -0
  91. package/dist/core/extensions/builtin/tool-pair-repair.d.ts.map +1 -0
  92. package/dist/core/extensions/builtin/tool-pair-repair.js +12 -0
  93. package/dist/core/extensions/builtin/tool-pair-repair.js.map +1 -0
  94. package/dist/core/extensions/types.d.ts +6 -2
  95. package/dist/core/extensions/types.d.ts.map +1 -1
  96. package/dist/core/extensions/types.js.map +1 -1
  97. package/dist/core/goal-continuation.d.ts +13 -0
  98. package/dist/core/goal-continuation.d.ts.map +1 -0
  99. package/dist/core/goal-continuation.js +10 -0
  100. package/dist/core/goal-continuation.js.map +1 -0
  101. package/dist/core/grok-harness.d.ts +1 -1
  102. package/dist/core/grok-harness.d.ts.map +1 -1
  103. package/dist/core/grok-harness.js +1 -1
  104. package/dist/core/grok-harness.js.map +1 -1
  105. package/dist/core/grok-playbook.d.ts +1 -1
  106. package/dist/core/grok-playbook.d.ts.map +1 -1
  107. package/dist/core/grok-playbook.js +1 -1
  108. package/dist/core/grok-playbook.js.map +1 -1
  109. package/dist/core/host-resource-probe.d.ts +63 -0
  110. package/dist/core/host-resource-probe.d.ts.map +1 -0
  111. package/dist/core/host-resource-probe.js +126 -0
  112. package/dist/core/host-resource-probe.js.map +1 -0
  113. package/dist/core/host-resource-snapshot.d.ts +54 -0
  114. package/dist/core/host-resource-snapshot.d.ts.map +1 -0
  115. package/dist/core/host-resource-snapshot.js +139 -0
  116. package/dist/core/host-resource-snapshot.js.map +1 -0
  117. package/dist/core/identical-loop.d.ts +15 -0
  118. package/dist/core/identical-loop.d.ts.map +1 -0
  119. package/dist/core/identical-loop.js +34 -0
  120. package/dist/core/identical-loop.js.map +1 -0
  121. package/dist/core/index.d.ts +1 -1
  122. package/dist/core/index.d.ts.map +1 -1
  123. package/dist/core/index.js +1 -1
  124. package/dist/core/index.js.map +1 -1
  125. package/dist/core/model-prompt-policy.d.ts +11 -0
  126. package/dist/core/model-prompt-policy.d.ts.map +1 -0
  127. package/dist/core/model-prompt-policy.js +17 -0
  128. package/dist/core/model-prompt-policy.js.map +1 -0
  129. package/dist/core/model-registry.d.ts.map +1 -1
  130. package/dist/core/model-registry.js +16 -4
  131. package/dist/core/model-registry.js.map +1 -1
  132. package/dist/core/model-resolver.d.ts.map +1 -1
  133. package/dist/core/model-resolver.js +2 -2
  134. package/dist/core/model-resolver.js.map +1 -1
  135. package/dist/core/prompt-budget.d.ts +21 -0
  136. package/dist/core/prompt-budget.d.ts.map +1 -0
  137. package/dist/core/prompt-budget.js +51 -0
  138. package/dist/core/prompt-budget.js.map +1 -0
  139. package/dist/core/prompt-cache.d.ts +14 -0
  140. package/dist/core/prompt-cache.d.ts.map +1 -0
  141. package/dist/core/prompt-cache.js +16 -0
  142. package/dist/core/prompt-cache.js.map +1 -0
  143. package/dist/core/prompt-preset.d.ts +8 -0
  144. package/dist/core/prompt-preset.d.ts.map +1 -0
  145. package/dist/core/prompt-preset.js +56 -0
  146. package/dist/core/prompt-preset.js.map +1 -0
  147. package/dist/core/prompt-settlement.d.ts +81 -0
  148. package/dist/core/prompt-settlement.d.ts.map +1 -0
  149. package/dist/core/prompt-settlement.js +84 -0
  150. package/dist/core/prompt-settlement.js.map +1 -0
  151. package/dist/core/provider-resilience.d.ts +16 -0
  152. package/dist/core/provider-resilience.d.ts.map +1 -1
  153. package/dist/core/provider-resilience.js +24 -2
  154. package/dist/core/provider-resilience.js.map +1 -1
  155. package/dist/core/provider-retry.d.ts +36 -0
  156. package/dist/core/provider-retry.d.ts.map +1 -0
  157. package/dist/core/provider-retry.js +73 -0
  158. package/dist/core/provider-retry.js.map +1 -0
  159. package/dist/core/provider-usage.d.ts +16 -2
  160. package/dist/core/provider-usage.d.ts.map +1 -1
  161. package/dist/core/provider-usage.js +150 -8
  162. package/dist/core/provider-usage.js.map +1 -1
  163. package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
  164. package/dist/core/reasoning-router-resolver.js +1 -1
  165. package/dist/core/reasoning-router-resolver.js.map +1 -1
  166. package/dist/core/resource-admission-config.d.ts +55 -0
  167. package/dist/core/resource-admission-config.d.ts.map +1 -0
  168. package/dist/core/resource-admission-config.js +97 -0
  169. package/dist/core/resource-admission-config.js.map +1 -0
  170. package/dist/core/resource-admission.d.ts +81 -0
  171. package/dist/core/resource-admission.d.ts.map +1 -0
  172. package/dist/core/resource-admission.js +128 -0
  173. package/dist/core/resource-admission.js.map +1 -0
  174. package/dist/core/resource-governor-format.d.ts +50 -0
  175. package/dist/core/resource-governor-format.d.ts.map +1 -0
  176. package/dist/core/resource-governor-format.js +113 -0
  177. package/dist/core/resource-governor-format.js.map +1 -0
  178. package/dist/core/resource-governor-settings.d.ts +56 -0
  179. package/dist/core/resource-governor-settings.d.ts.map +1 -0
  180. package/dist/core/resource-governor-settings.js +89 -0
  181. package/dist/core/resource-governor-settings.js.map +1 -0
  182. package/dist/core/resource-loader.d.ts.map +1 -1
  183. package/dist/core/resource-loader.js +32 -0
  184. package/dist/core/resource-loader.js.map +1 -1
  185. package/dist/core/resource-observation-journal.d.ts +74 -0
  186. package/dist/core/resource-observation-journal.d.ts.map +1 -0
  187. package/dist/core/resource-observation-journal.js +198 -0
  188. package/dist/core/resource-observation-journal.js.map +1 -0
  189. package/dist/core/resource-safety-gate.d.ts +54 -0
  190. package/dist/core/resource-safety-gate.d.ts.map +1 -0
  191. package/dist/core/resource-safety-gate.js +51 -0
  192. package/dist/core/resource-safety-gate.js.map +1 -0
  193. package/dist/core/run-journal.d.ts.map +1 -1
  194. package/dist/core/run-journal.js +13 -1
  195. package/dist/core/run-journal.js.map +1 -1
  196. package/dist/core/run-resource-lease.d.ts +73 -0
  197. package/dist/core/run-resource-lease.d.ts.map +1 -0
  198. package/dist/core/run-resource-lease.js +78 -0
  199. package/dist/core/run-resource-lease.js.map +1 -0
  200. package/dist/core/sandbox/backend.d.ts +12 -0
  201. package/dist/core/sandbox/backend.d.ts.map +1 -1
  202. package/dist/core/sandbox/backend.js +52 -42
  203. package/dist/core/sandbox/backend.js.map +1 -1
  204. package/dist/core/sandbox/policy.d.ts +2 -0
  205. package/dist/core/sandbox/policy.d.ts.map +1 -1
  206. package/dist/core/sandbox/policy.js +2 -1
  207. package/dist/core/sandbox/policy.js.map +1 -1
  208. package/dist/core/sdk.d.ts +2 -0
  209. package/dist/core/sdk.d.ts.map +1 -1
  210. package/dist/core/sdk.js +1 -0
  211. package/dist/core/sdk.js.map +1 -1
  212. package/dist/core/session-bash-service.d.ts +12 -0
  213. package/dist/core/session-bash-service.d.ts.map +1 -1
  214. package/dist/core/session-bash-service.js +14 -0
  215. package/dist/core/session-bash-service.js.map +1 -1
  216. package/dist/core/session-system-prompt.d.ts +30 -0
  217. package/dist/core/session-system-prompt.d.ts.map +1 -0
  218. package/dist/core/session-system-prompt.js +39 -0
  219. package/dist/core/session-system-prompt.js.map +1 -0
  220. package/dist/core/session-termination.d.ts +7 -2
  221. package/dist/core/session-termination.d.ts.map +1 -1
  222. package/dist/core/session-termination.js +29 -1
  223. package/dist/core/session-termination.js.map +1 -1
  224. package/dist/core/settings-manager.d.ts +12 -0
  225. package/dist/core/settings-manager.d.ts.map +1 -1
  226. package/dist/core/settings-manager.js +36 -0
  227. package/dist/core/settings-manager.js.map +1 -1
  228. package/dist/core/slash-commands.d.ts.map +1 -1
  229. package/dist/core/slash-commands.js +1 -0
  230. package/dist/core/slash-commands.js.map +1 -1
  231. package/dist/core/strict-record.d.ts +2 -0
  232. package/dist/core/strict-record.d.ts.map +1 -0
  233. package/dist/core/strict-record.js +9 -0
  234. package/dist/core/strict-record.js.map +1 -0
  235. package/dist/core/subagent-lane-launcher.d.ts +64 -0
  236. package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
  237. package/dist/core/subagent-lane-launcher.js +107 -0
  238. package/dist/core/subagent-lane-launcher.js.map +1 -0
  239. package/dist/core/system-cpu-sampler.d.ts +45 -0
  240. package/dist/core/system-cpu-sampler.d.ts.map +1 -0
  241. package/dist/core/system-cpu-sampler.js +95 -0
  242. package/dist/core/system-cpu-sampler.js.map +1 -0
  243. package/dist/core/system-prompt.d.ts.map +1 -1
  244. package/dist/core/system-prompt.js +5 -8
  245. package/dist/core/system-prompt.js.map +1 -1
  246. package/dist/core/tool-pair-repair.d.ts +11 -0
  247. package/dist/core/tool-pair-repair.d.ts.map +1 -0
  248. package/dist/core/tool-pair-repair.js +38 -0
  249. package/dist/core/tool-pair-repair.js.map +1 -0
  250. package/dist/core/tools/artifact-spill.d.ts +13 -0
  251. package/dist/core/tools/artifact-spill.d.ts.map +1 -0
  252. package/dist/core/tools/artifact-spill.js +26 -0
  253. package/dist/core/tools/artifact-spill.js.map +1 -0
  254. package/dist/core/tools/bash.d.ts +7 -4
  255. package/dist/core/tools/bash.d.ts.map +1 -1
  256. package/dist/core/tools/bash.js +8 -7
  257. package/dist/core/tools/bash.js.map +1 -1
  258. package/dist/core/tools/index.d.ts +1 -1
  259. package/dist/core/tools/index.d.ts.map +1 -1
  260. package/dist/core/tools/index.js.map +1 -1
  261. package/dist/core/tools/read.d.ts +1 -0
  262. package/dist/core/tools/read.d.ts.map +1 -1
  263. package/dist/core/tools/read.js +10 -1
  264. package/dist/core/tools/read.js.map +1 -1
  265. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  266. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  267. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  268. package/dist/core/workload-classifier.d.ts +32 -0
  269. package/dist/core/workload-classifier.d.ts.map +1 -0
  270. package/dist/core/workload-classifier.js +66 -0
  271. package/dist/core/workload-classifier.js.map +1 -0
  272. package/dist/core/workload-family-matcher.d.ts +3 -0
  273. package/dist/core/workload-family-matcher.d.ts.map +1 -0
  274. package/dist/core/workload-family-matcher.js +126 -0
  275. package/dist/core/workload-family-matcher.js.map +1 -0
  276. package/dist/core/workload-permit-pool.d.ts +78 -0
  277. package/dist/core/workload-permit-pool.d.ts.map +1 -0
  278. package/dist/core/workload-permit-pool.js +138 -0
  279. package/dist/core/workload-permit-pool.js.map +1 -0
  280. package/dist/core/workload-shard-executor.d.ts +68 -0
  281. package/dist/core/workload-shard-executor.d.ts.map +1 -0
  282. package/dist/core/workload-shard-executor.js +156 -0
  283. package/dist/core/workload-shard-executor.js.map +1 -0
  284. package/dist/core/workload-shard-plan.d.ts +81 -0
  285. package/dist/core/workload-shard-plan.d.ts.map +1 -0
  286. package/dist/core/workload-shard-plan.js +183 -0
  287. package/dist/core/workload-shard-plan.js.map +1 -0
  288. package/dist/core/workload-shard-runner.d.ts +11 -0
  289. package/dist/core/workload-shard-runner.d.ts.map +1 -0
  290. package/dist/core/workload-shard-runner.js +98 -0
  291. package/dist/core/workload-shard-runner.js.map +1 -0
  292. package/dist/core/workload-shard-store.d.ts +44 -0
  293. package/dist/core/workload-shard-store.d.ts.map +1 -0
  294. package/dist/core/workload-shard-store.js +189 -0
  295. package/dist/core/workload-shard-store.js.map +1 -0
  296. package/dist/core/workload-sharders.d.ts +47 -0
  297. package/dist/core/workload-sharders.d.ts.map +1 -0
  298. package/dist/core/workload-sharders.js +162 -0
  299. package/dist/core/workload-sharders.js.map +1 -0
  300. package/dist/core/workload-shell-scan.d.ts +12 -0
  301. package/dist/core/workload-shell-scan.d.ts.map +1 -0
  302. package/dist/core/workload-shell-scan.js +79 -0
  303. package/dist/core/workload-shell-scan.js.map +1 -0
  304. package/dist/index.d.ts +18 -1
  305. package/dist/index.d.ts.map +1 -1
  306. package/dist/index.js +18 -0
  307. package/dist/index.js.map +1 -1
  308. package/dist/main.d.ts.map +1 -1
  309. package/dist/main.js +14 -0
  310. package/dist/main.js.map +1 -1
  311. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  312. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  313. package/dist/modes/interactive/interactive-mode.js +81 -0
  314. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  315. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  316. package/dist/modes/interactive/theme/theme.js +14 -3
  317. package/dist/modes/interactive/theme/theme.js.map +1 -1
  318. package/docs/adaptorch-preview-spec.md +1 -1
  319. package/docs/adaptorch-preview.md +2 -2
  320. package/docs/book-to-skill.md +1 -1
  321. package/docs/containerization.md +9 -0
  322. package/docs/docs.json +11 -3
  323. package/docs/environment-variables.md +11 -4
  324. package/docs/extensions.md +14 -1
  325. package/docs/grok-harness.md +35 -12
  326. package/docs/index.md +7 -6
  327. package/docs/loadout-domains/grok-harness.md +2 -2
  328. package/docs/models.md +10 -9
  329. package/docs/provider-resilience.md +9 -3
  330. package/docs/providers.md +9 -4
  331. package/docs/run-protocol.md +32 -0
  332. package/docs/sdk.md +100 -3
  333. package/docs/security.md +22 -32
  334. package/docs/sessions.md +14 -0
  335. package/docs/settings.md +64 -2
  336. package/docs/skills.md +16 -0
  337. package/docs/usage.md +37 -15
  338. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  339. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  340. package/examples/extensions/custom-provider-gitlab-duo/index.ts +6 -1
  341. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  342. package/examples/extensions/gondolin/package-lock.json +2 -2
  343. package/examples/extensions/gondolin/package.json +1 -1
  344. package/examples/extensions/sandbox/package-lock.json +2 -2
  345. package/examples/extensions/sandbox/package.json +1 -1
  346. package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
  347. package/examples/extensions/subagent/domain-profiles.ts +0 -1
  348. package/examples/extensions/with-deps/package-lock.json +2 -2
  349. package/examples/extensions/with-deps/package.json +1 -1
  350. package/npm-shrinkwrap.json +18 -18
  351. package/package.json +7 -9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-settlement.js","sourceRoot":"","sources":["../../src/core/prompt-settlement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Prompt settlement coordinator (OMK v0.97.x roadmap §16, M4/PR6).\n *\n * `agent_end` is NOT final (§16.1): provider retries, compaction retries,\n * durable-goal continuations, steering/follow-up queues, shards, and child\n * subagents may still run. `prompt_settled` fires only when the reducer's\n * §16.4 conditions all hold, exactly once per promptRunId. Consumers (the\n * M4 completion sound, notifications) must treat it as a UX signal, never a\n * correctness signal (§3 non-goals).\n *\n * The reducer is a pure function over an immutable state (§16.5). Shard and\n * child counters are wired by M5/M6; they participate in the contract now so\n * those slices only add signal call sites.\n */\n\nexport type PromptSettlementOutcome = \"completed\" | \"failed\" | \"aborted\";\n\n/** §16.2 prompt run identity. */\nexport interface PromptRunRef {\n\treadonly promptRunId: string;\n\treadonly sessionId: string;\n\treadonly startedAt: string;\n\treadonly lineageId?: string;\n}\n\n/** §16.3 settlement contract. */\nexport interface PromptSettledEvent {\n\treadonly type: \"prompt_settled\";\n\treadonly promptRunId: string;\n\treadonly outcome: PromptSettlementOutcome;\n\treadonly durationMs: number;\n\treadonly terminationKind?: string;\n}\n\n/** §16.5 coordinator state. */\nexport interface PromptSettlementState {\n\treadonly promptRunId: string;\n\treadonly startedAtEpochMs: number;\n\treadonly activeProviderAttempts: number;\n\treadonly activeTools: number;\n\treadonly activeShards: number;\n\treadonly activeChildren: number;\n\treadonly queuedContinuations: number;\n\treadonly terminal?: {\n\t\treadonly outcome: PromptSettlementOutcome;\n\t\treadonly terminationKind?: string;\n\t};\n\treadonly emitted: boolean;\n}\n\nexport type PromptSettlementSignal =\n\t| { readonly kind: \"provider_attempt\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"tool\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"shard\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"child\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"continuation\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"terminal\"; readonly outcome: PromptSettlementOutcome; readonly terminationKind?: string }\n\t| { readonly kind: \"emitted\" };\n\nexport function createPromptSettlementState(promptRunId: string, startedAtEpochMs: number): PromptSettlementState {\n\treturn {\n\t\tpromptRunId,\n\t\tstartedAtEpochMs,\n\t\tactiveProviderAttempts: 0,\n\t\tactiveTools: 0,\n\t\tactiveShards: 0,\n\t\tactiveChildren: 0,\n\t\tqueuedContinuations: 0,\n\t\temitted: false,\n\t};\n}\n\n/** Pure reducer (§16.5). Counters clamp at zero; terminal and emitted are latching. */\nexport function reducePromptSettlement(\n\tstate: PromptSettlementState,\n\tsignal: PromptSettlementSignal,\n): PromptSettlementState {\n\tswitch (signal.kind) {\n\t\tcase \"provider_attempt\":\n\t\t\treturn { ...state, activeProviderAttempts: clampCount(state.activeProviderAttempts + signal.delta) };\n\t\tcase \"tool\":\n\t\t\treturn { ...state, activeTools: clampCount(state.activeTools + signal.delta) };\n\t\tcase \"shard\":\n\t\t\treturn { ...state, activeShards: clampCount(state.activeShards + signal.delta) };\n\t\tcase \"child\":\n\t\t\treturn { ...state, activeChildren: clampCount(state.activeChildren + signal.delta) };\n\t\tcase \"continuation\":\n\t\t\treturn { ...state, queuedContinuations: clampCount(state.queuedContinuations + signal.delta) };\n\t\tcase \"terminal\":\n\t\t\t// First terminal outcome wins; later reclassification never flips it (§15.3).\n\t\t\tif (state.terminal !== undefined) {\n\t\t\t\treturn state;\n\t\t\t}\n\t\t\treturn { ...state, terminal: { outcome: signal.outcome, terminationKind: signal.terminationKind } };\n\t\tcase \"emitted\":\n\t\t\treturn { ...state, emitted: true };\n\t\tdefault: {\n\t\t\tconst exhaustive: never = signal;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\n}\n\n/** §16.4: every condition must hold before `prompt_settled` may emit. */\nexport function shouldEmitPromptSettled(state: PromptSettlementState): boolean {\n\treturn (\n\t\tstate.terminal !== undefined &&\n\t\t!state.emitted &&\n\t\tstate.activeProviderAttempts === 0 &&\n\t\tstate.activeTools === 0 &&\n\t\tstate.activeShards === 0 &&\n\t\tstate.activeChildren === 0 &&\n\t\tstate.queuedContinuations === 0\n\t);\n}\n\n/**\n * Build the event when (and only when) §16.4 holds, marking the state\n * emitted so the same sequence can never yield a second event.\n */\nexport function settlePromptIfReady(\n\tstate: PromptSettlementState,\n\tnowEpochMs: number,\n): { readonly state: PromptSettlementState; readonly event: PromptSettledEvent | null } {\n\tif (!shouldEmitPromptSettled(state) || state.terminal === undefined) {\n\t\treturn { state, event: null };\n\t}\n\tconst event: PromptSettledEvent = {\n\t\ttype: \"prompt_settled\",\n\t\tpromptRunId: state.promptRunId,\n\t\toutcome: state.terminal.outcome,\n\t\tdurationMs: Math.max(0, nowEpochMs - state.startedAtEpochMs),\n\t\t...(state.terminal.terminationKind !== undefined ? { terminationKind: state.terminal.terminationKind } : {}),\n\t};\n\treturn { state: reducePromptSettlement(state, { kind: \"emitted\" }), event };\n}\n\nfunction clampCount(value: number): number {\n\treturn Math.max(0, value);\n}\n"]}
@@ -11,6 +11,8 @@ export type FailoverCandidate = Readonly<{
11
11
  }>;
12
12
  /** Models known to emit high false-positive content/safety stops on coding work. */
13
13
  export declare const STICKY_SAFETY_MODEL_RE: RegExp;
14
+ /** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */
15
+ export declare const NO_SAFETY_FAILOVER_MODEL_RE: RegExp;
14
16
  /** Default failover chain when a sticky safety model refuses a turn. */
15
17
  export declare const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[];
16
18
  export type ProviderResilienceSettings = Readonly<{
@@ -25,6 +27,8 @@ export declare const DEFAULT_PROVIDER_RESILIENCE: Required<Pick<ProviderResilien
25
27
  readonly failoverCandidates: readonly FailoverCandidate[];
26
28
  };
27
29
  export declare function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean;
30
+ /** Fable / Opus / Sonnet stay on the requested id after a safety stop. */
31
+ export declare function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean;
28
32
  export declare function isContentSafetyStopMessage(text: string | undefined): boolean;
29
33
  /**
30
34
  * Billing/quota exhaustion ("usage limit for this billing cycle",
@@ -41,6 +45,18 @@ export declare function isOrphanToolCallIdError(text: string | undefined): boole
41
45
  */
42
46
  export declare function isTransientProviderErrorMessage(text: string | undefined): boolean;
43
47
  export declare function resolveFailoverCandidates(settings: ProviderResilienceSettings | undefined): readonly FailoverCandidate[];
48
+ /** Explicit `--model` / `--provider` pins stay on the requested model. */
49
+ export declare function shouldHonorSafetyFailover(options: {
50
+ readonly autoFailoverOnSafetyStop: boolean;
51
+ readonly modelPinned: boolean;
52
+ readonly modelId?: string;
53
+ readonly provider?: string;
54
+ }): boolean;
55
+ /** Sticky-model eject still applies unless the user pinned that model. */
56
+ export declare function shouldEjectStickySafetyModel(options: {
57
+ readonly blockStickySafetyModels: boolean;
58
+ readonly modelPinned: boolean;
59
+ }): boolean;
44
60
  export declare function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {
45
61
  readonly blockStickySafetyModels: boolean;
46
62
  readonly autoFailoverOnSafetyStop: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,QAAW,CAAC;AAE/C,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,EAAE,SAAS,iBAAiB,EAQjE,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IACjD,wFAAwF;IACxF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC3D,CAAC,CAAC;AAEH,eAAO,MAAM,2BAA2B,EAAE,QAAQ,CACjD,IAAI,CAAC,0BAA0B,EAAE,yBAAyB,GAAG,0BAA0B,CAAC,CACxF,GAAG;IACH,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAK1D,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMvG;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAK5E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAK1E;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKjF;AAED,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAC9C,SAAS,iBAAiB,EAAE,CAI9B;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAAG;IAC5F,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC1D,CAOA;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EACxC,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACzE,SAAS,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,OAAO,GAClD,iBAAiB,GAAG,SAAS,CAQ/B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMlF","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
@@ -7,11 +7,13 @@
7
7
  */
8
8
  /** Models known to emit high false-positive content/safety stops on coding work. */
9
9
  export const STICKY_SAFETY_MODEL_RE = /fable/i;
10
+ /** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */
11
+ export const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;
10
12
  /** Default failover chain when a sticky safety model refuses a turn. */
11
13
  export const DEFAULT_SAFETY_FAILOVER_CANDIDATES = [
12
14
  { provider: "kimi-coding", id: "k3" },
13
15
  { provider: "modelstudio-maas", id: "qwen3.8-max-preview" },
14
- { provider: "grok-oauth-proxy", id: "grok-4.5" },
16
+ { provider: "xai", id: "grok-4.5" },
15
17
  { provider: "deepseek", id: "deepseek-v4-pro" },
16
18
  { provider: "deepseek", id: "deepseek-v4-flash" },
17
19
  { provider: "modelstudio-maas", id: "deepseek-v4-pro" },
@@ -31,6 +33,16 @@ export function isStickySafetyModel(modelId, provider) {
31
33
  const p = (provider ?? "").toLowerCase();
32
34
  return p.includes("anthropic") && STICKY_SAFETY_MODEL_RE.test(id);
33
35
  }
36
+ /** Fable / Opus / Sonnet stay on the requested id after a safety stop. */
37
+ export function isNoSafetyFailoverModel(modelId, provider) {
38
+ const id = (modelId ?? "").trim();
39
+ if (!id)
40
+ return false;
41
+ if (NO_SAFETY_FAILOVER_MODEL_RE.test(id))
42
+ return true;
43
+ const p = (provider ?? "").toLowerCase();
44
+ return p.includes("anthropic") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);
45
+ }
34
46
  export function isContentSafetyStopMessage(text) {
35
47
  if (!text)
36
48
  return false;
@@ -60,7 +72,7 @@ export function isOrphanToolCallIdError(text) {
60
72
  export function isTransientProviderErrorMessage(text) {
61
73
  if (!text)
62
74
  return false;
63
- return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\bterminated\b|retry delay|content\/safety stop|stop_reason\s*=\s*(refusal|sensitive)|safety stop|tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|invalid_request_error/i.test(text);
75
+ return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\bterminated\b|retry delay|content\/safety stop|stop_reason\s*=\s*(refusal|sensitive)|safety stop|tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(text);
64
76
  }
65
77
  export function resolveFailoverCandidates(settings) {
66
78
  const custom = settings?.failoverCandidates;
@@ -68,6 +80,16 @@ export function resolveFailoverCandidates(settings) {
68
80
  return custom;
69
81
  return DEFAULT_SAFETY_FAILOVER_CANDIDATES;
70
82
  }
83
+ /** Explicit `--model` / `--provider` pins stay on the requested model. */
84
+ export function shouldHonorSafetyFailover(options) {
85
+ if (!options.autoFailoverOnSafetyStop || options.modelPinned)
86
+ return false;
87
+ return !isNoSafetyFailoverModel(options.modelId, options.provider);
88
+ }
89
+ /** Sticky-model eject still applies unless the user pinned that model. */
90
+ export function shouldEjectStickySafetyModel(options) {
91
+ return options.blockStickySafetyModels && !options.modelPinned;
92
+ }
71
93
  export function resolveProviderResilience(settings) {
72
94
  return {
73
95
  blockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.js","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.js","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { AssistantMessage } from "omk-ai";
2
+ /**
3
+ * Pure retry/failover decisions for agent-turn provider errors.
4
+ * Context overflow is never retryable here — compaction owns that path.
5
+ * Quota exhaustion is retryable so the failover chain can save the turn.
6
+ */
7
+ /**
8
+ * Empty streamed completion — stop=stop but zero usable output (no text,
9
+ * thinking, or toolCall). Live signature of relay first-token timeouts
10
+ * (~181s silent kill on thinking-heavy ox-alpha turns, 2026-08-22 log evidence).
11
+ * These are transport failures wearing a success shape.
12
+ */
13
+ export declare function isEmptyStreamedCompletion(message: AssistantMessage): boolean;
14
+ export declare function isRetryableAssistantError(message: AssistantMessage, contextWindow: number): boolean;
15
+ /**
16
+ * Next attempt number within the retry budget, or undefined when retry is
17
+ * disabled or the budget is exhausted. The caller's completed-attempt count
18
+ * stays unchanged when undefined is returned.
19
+ */
20
+ export declare function retryBudgetForAssistantError(message: AssistantMessage, configuredMaxRetries: number): number;
21
+ export declare function nextRetryAttempt(input: {
22
+ readonly enabled: boolean;
23
+ readonly completedAttempts: number;
24
+ readonly maxRetries: number;
25
+ }): number | undefined;
26
+ /** Same-model retry backs off exponentially; a failed-over retry starts fast. */
27
+ export declare function computeRetryDelayMs(baseDelayMs: number, attempt: number, failoverOccurred: boolean): number;
28
+ /**
29
+ * Errors that justify an immediate model switch instead of a same-model retry:
30
+ * safety-stop false positives and billing/quota exhaustion. Both mean the
31
+ * current model cannot finish this turn.
32
+ */
33
+ export declare function isFailoverTriggerError(errorMessage: string | undefined): boolean;
34
+ /** Bookkeeping key for the per-turn refused/failed model set. */
35
+ export declare function failoverModelKey(provider: string, id: string): string;
36
+ //# sourceMappingURL=provider-retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-retry.d.ts","sourceRoot":"","sources":["../../src/core/provider-retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAQ/C;;;;GAIG;AACH;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAW5E;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAQnG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,GAAG,MAAM,CAG5G;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B,GAAG,MAAM,GAAG,SAAS,CAIrB;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAE3G;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEhF;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAErE","sourcesContent":["import type { AssistantMessage } from \"omk-ai\";\nimport { isContextOverflow } from \"omk-ai\";\nimport {\n\tisContentSafetyStopMessage,\n\tisQuotaExhaustionMessage,\n\tisTransientProviderErrorMessage,\n} from \"./provider-resilience.ts\";\n\n/**\n * Pure retry/failover decisions for agent-turn provider errors.\n * Context overflow is never retryable here — compaction owns that path.\n * Quota exhaustion is retryable so the failover chain can save the turn.\n */\n/**\n * Empty streamed completion — stop=stop but zero usable output (no text,\n * thinking, or toolCall). Live signature of relay first-token timeouts\n * (~181s silent kill on thinking-heavy ox-alpha turns, 2026-08-22 log evidence).\n * These are transport failures wearing a success shape.\n */\nexport function isEmptyStreamedCompletion(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"stop\") return false;\n\tconst content: unknown = message.content;\n\tif (!Array.isArray(content)) {\n\t\treturn typeof content === \"string\" ? content.trim().length === 0 : true;\n\t}\n\treturn content.every((block) => {\n\t\tconst t = (block as { type?: string } | null)?.type;\n\t\tif (t !== \"text\") return false; // thinking/toolCall/image = real output\n\t\treturn !String((block as { text?: string }).text ?? \"\").trim();\n\t});\n}\n\nexport function isRetryableAssistantError(message: AssistantMessage, contextWindow: number): boolean {\n\t// An empty streamed completion is a dead stream, not an answer — retry it\n\t// regardless of its success-shaped stopReason (bounded by maxRetries).\n\tif (isEmptyStreamedCompletion(message)) return true;\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tif (isContextOverflow(message, contextWindow)) return false;\n\tif (isQuotaExhaustionMessage(message.errorMessage)) return true;\n\treturn isTransientProviderErrorMessage(message.errorMessage);\n}\n\n/**\n * Next attempt number within the retry budget, or undefined when retry is\n * disabled or the budget is exhausted. The caller's completed-attempt count\n * stays unchanged when undefined is returned.\n */\nexport function retryBudgetForAssistantError(message: AssistantMessage, configuredMaxRetries: number): number {\n\tif (configuredMaxRetries <= 0) return 0;\n\treturn isContentSafetyStopMessage(message.errorMessage) ? Math.min(1, configuredMaxRetries) : configuredMaxRetries;\n}\n\nexport function nextRetryAttempt(input: {\n\treadonly enabled: boolean;\n\treadonly completedAttempts: number;\n\treadonly maxRetries: number;\n}): number | undefined {\n\tif (!input.enabled) return undefined;\n\tconst attempt = input.completedAttempts + 1;\n\treturn attempt > input.maxRetries ? undefined : attempt;\n}\n\n/** Same-model retry backs off exponentially; a failed-over retry starts fast. */\nexport function computeRetryDelayMs(baseDelayMs: number, attempt: number, failoverOccurred: boolean): number {\n\treturn failoverOccurred ? Math.min(400, baseDelayMs) : baseDelayMs * 2 ** (attempt - 1);\n}\n\n/**\n * Errors that justify an immediate model switch instead of a same-model retry:\n * safety-stop false positives and billing/quota exhaustion. Both mean the\n * current model cannot finish this turn.\n */\nexport function isFailoverTriggerError(errorMessage: string | undefined): boolean {\n\treturn isContentSafetyStopMessage(errorMessage) || isQuotaExhaustionMessage(errorMessage);\n}\n\n/** Bookkeeping key for the per-turn refused/failed model set. */\nexport function failoverModelKey(provider: string, id: string): string {\n\treturn `${provider}/${id}`;\n}\n"]}
@@ -0,0 +1,73 @@
1
+ import { isContextOverflow } from "omk-ai";
2
+ import { isContentSafetyStopMessage, isQuotaExhaustionMessage, isTransientProviderErrorMessage, } from "./provider-resilience.js";
3
+ /**
4
+ * Pure retry/failover decisions for agent-turn provider errors.
5
+ * Context overflow is never retryable here — compaction owns that path.
6
+ * Quota exhaustion is retryable so the failover chain can save the turn.
7
+ */
8
+ /**
9
+ * Empty streamed completion — stop=stop but zero usable output (no text,
10
+ * thinking, or toolCall). Live signature of relay first-token timeouts
11
+ * (~181s silent kill on thinking-heavy ox-alpha turns, 2026-08-22 log evidence).
12
+ * These are transport failures wearing a success shape.
13
+ */
14
+ export function isEmptyStreamedCompletion(message) {
15
+ if (message.stopReason !== "stop")
16
+ return false;
17
+ const content = message.content;
18
+ if (!Array.isArray(content)) {
19
+ return typeof content === "string" ? content.trim().length === 0 : true;
20
+ }
21
+ return content.every((block) => {
22
+ const t = block?.type;
23
+ if (t !== "text")
24
+ return false; // thinking/toolCall/image = real output
25
+ return !String(block.text ?? "").trim();
26
+ });
27
+ }
28
+ export function isRetryableAssistantError(message, contextWindow) {
29
+ // An empty streamed completion is a dead stream, not an answer — retry it
30
+ // regardless of its success-shaped stopReason (bounded by maxRetries).
31
+ if (isEmptyStreamedCompletion(message))
32
+ return true;
33
+ if (message.stopReason !== "error" || !message.errorMessage)
34
+ return false;
35
+ if (isContextOverflow(message, contextWindow))
36
+ return false;
37
+ if (isQuotaExhaustionMessage(message.errorMessage))
38
+ return true;
39
+ return isTransientProviderErrorMessage(message.errorMessage);
40
+ }
41
+ /**
42
+ * Next attempt number within the retry budget, or undefined when retry is
43
+ * disabled or the budget is exhausted. The caller's completed-attempt count
44
+ * stays unchanged when undefined is returned.
45
+ */
46
+ export function retryBudgetForAssistantError(message, configuredMaxRetries) {
47
+ if (configuredMaxRetries <= 0)
48
+ return 0;
49
+ return isContentSafetyStopMessage(message.errorMessage) ? Math.min(1, configuredMaxRetries) : configuredMaxRetries;
50
+ }
51
+ export function nextRetryAttempt(input) {
52
+ if (!input.enabled)
53
+ return undefined;
54
+ const attempt = input.completedAttempts + 1;
55
+ return attempt > input.maxRetries ? undefined : attempt;
56
+ }
57
+ /** Same-model retry backs off exponentially; a failed-over retry starts fast. */
58
+ export function computeRetryDelayMs(baseDelayMs, attempt, failoverOccurred) {
59
+ return failoverOccurred ? Math.min(400, baseDelayMs) : baseDelayMs * 2 ** (attempt - 1);
60
+ }
61
+ /**
62
+ * Errors that justify an immediate model switch instead of a same-model retry:
63
+ * safety-stop false positives and billing/quota exhaustion. Both mean the
64
+ * current model cannot finish this turn.
65
+ */
66
+ export function isFailoverTriggerError(errorMessage) {
67
+ return isContentSafetyStopMessage(errorMessage) || isQuotaExhaustionMessage(errorMessage);
68
+ }
69
+ /** Bookkeeping key for the per-turn refused/failed model set. */
70
+ export function failoverModelKey(provider, id) {
71
+ return `${provider}/${id}`;
72
+ }
73
+ //# sourceMappingURL=provider-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-retry.js","sourceRoot":"","sources":["../../src/core/provider-retry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"omk-ai\";\nimport { isContextOverflow } from \"omk-ai\";\nimport {\n\tisContentSafetyStopMessage,\n\tisQuotaExhaustionMessage,\n\tisTransientProviderErrorMessage,\n} from \"./provider-resilience.ts\";\n\n/**\n * Pure retry/failover decisions for agent-turn provider errors.\n * Context overflow is never retryable here — compaction owns that path.\n * Quota exhaustion is retryable so the failover chain can save the turn.\n */\n/**\n * Empty streamed completion — stop=stop but zero usable output (no text,\n * thinking, or toolCall). Live signature of relay first-token timeouts\n * (~181s silent kill on thinking-heavy ox-alpha turns, 2026-08-22 log evidence).\n * These are transport failures wearing a success shape.\n */\nexport function isEmptyStreamedCompletion(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"stop\") return false;\n\tconst content: unknown = message.content;\n\tif (!Array.isArray(content)) {\n\t\treturn typeof content === \"string\" ? content.trim().length === 0 : true;\n\t}\n\treturn content.every((block) => {\n\t\tconst t = (block as { type?: string } | null)?.type;\n\t\tif (t !== \"text\") return false; // thinking/toolCall/image = real output\n\t\treturn !String((block as { text?: string }).text ?? \"\").trim();\n\t});\n}\n\nexport function isRetryableAssistantError(message: AssistantMessage, contextWindow: number): boolean {\n\t// An empty streamed completion is a dead stream, not an answer — retry it\n\t// regardless of its success-shaped stopReason (bounded by maxRetries).\n\tif (isEmptyStreamedCompletion(message)) return true;\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tif (isContextOverflow(message, contextWindow)) return false;\n\tif (isQuotaExhaustionMessage(message.errorMessage)) return true;\n\treturn isTransientProviderErrorMessage(message.errorMessage);\n}\n\n/**\n * Next attempt number within the retry budget, or undefined when retry is\n * disabled or the budget is exhausted. The caller's completed-attempt count\n * stays unchanged when undefined is returned.\n */\nexport function retryBudgetForAssistantError(message: AssistantMessage, configuredMaxRetries: number): number {\n\tif (configuredMaxRetries <= 0) return 0;\n\treturn isContentSafetyStopMessage(message.errorMessage) ? Math.min(1, configuredMaxRetries) : configuredMaxRetries;\n}\n\nexport function nextRetryAttempt(input: {\n\treadonly enabled: boolean;\n\treadonly completedAttempts: number;\n\treadonly maxRetries: number;\n}): number | undefined {\n\tif (!input.enabled) return undefined;\n\tconst attempt = input.completedAttempts + 1;\n\treturn attempt > input.maxRetries ? undefined : attempt;\n}\n\n/** Same-model retry backs off exponentially; a failed-over retry starts fast. */\nexport function computeRetryDelayMs(baseDelayMs: number, attempt: number, failoverOccurred: boolean): number {\n\treturn failoverOccurred ? Math.min(400, baseDelayMs) : baseDelayMs * 2 ** (attempt - 1);\n}\n\n/**\n * Errors that justify an immediate model switch instead of a same-model retry:\n * safety-stop false positives and billing/quota exhaustion. Both mean the\n * current model cannot finish this turn.\n */\nexport function isFailoverTriggerError(errorMessage: string | undefined): boolean {\n\treturn isContentSafetyStopMessage(errorMessage) || isQuotaExhaustionMessage(errorMessage);\n}\n\n/** Bookkeeping key for the per-turn refused/failed model set. */\nexport function failoverModelKey(provider: string, id: string): string {\n\treturn `${provider}/${id}`;\n}\n"]}
@@ -18,7 +18,7 @@ export type CodexUsageSnapshot = {
18
18
  readonly fiveHour?: CodexUsageWindow;
19
19
  readonly sevenDay?: CodexUsageWindow;
20
20
  };
21
- type UsageKind = "codex" | "claude" | "kimi" | "zai" | "unavailable";
21
+ type UsageKind = "codex" | "claude" | "kimi" | "zai" | "grok" | "qwen-token-plan" | "unavailable";
22
22
  type CredentialCandidate = {
23
23
  readonly provider: string;
24
24
  readonly oauthOnly: boolean;
@@ -37,10 +37,24 @@ export declare function getSubscriptionUsageRevision(provider: string): number;
37
37
  export declare function supportsSubscriptionUsage(session: UsageSession, provider?: string): boolean;
38
38
  /** Configured quota groups shown in the rail, with the active group first. */
39
39
  export declare function getConfiguredSubscriptionUsageProviders(session: UsageSession): readonly string[];
40
- export declare function loadSubscriptionUsage(session: UsageSession, fetchImpl?: FetchLike, provider?: string): Promise<SubscriptionUsageSnapshot | undefined>;
40
+ export declare function loadSubscriptionUsage(session: UsageSession, fetchImpl?: FetchLike, provider?: string, qwenCliRunner?: QwenCliRunner): Promise<SubscriptionUsageSnapshot | undefined>;
41
41
  export declare function parseCodexUsageSnapshot(value: unknown, nowSeconds?: number): CodexUsageSnapshot | undefined;
42
42
  export declare function parseClaudeUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined;
43
43
  export declare function parseKimiUsageSnapshot(value: unknown, nowSeconds?: number): readonly SubscriptionUsageWindow[] | undefined;
44
+ /** Result of one `qwencloud` invocation; `missing` means the CLI is not installed. */
45
+ export type QwenCliResult = {
46
+ readonly kind: "missing";
47
+ } | {
48
+ readonly kind: "ran";
49
+ readonly exitCode: number;
50
+ readonly stdout: string;
51
+ };
52
+ export type QwenCliRunner = (args: readonly string[], timeoutMs: number) => Promise<QwenCliResult>;
53
+ export declare function buildQwenCliEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
54
+ /** Parse `qwencloud usage summary --format json` → the token-plan 7-day window. */
55
+ export declare function parseQwenTokenPlanUsage(value: unknown): readonly SubscriptionUsageWindow[] | undefined;
56
+ export declare function fetchQwenTokenPlanUsage(label: string, runner?: QwenCliRunner): Promise<SubscriptionUsageSnapshot>;
57
+ export declare function parseGrokUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined;
44
58
  export declare function parseZaiUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined;
45
59
  export declare function recordCodexPassiveUsage(apiKey: string, snapshot: ProviderRateLimitSnapshot, nowMs?: number): void;
46
60
  export declare function recordClaudePassiveUsage(apiKey: string, snapshot: ProviderRateLimitSnapshot, nowMs?: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-usage.d.ts","sourceRoot":"","sources":["../../src/core/provider-usage.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { ProviderRateLimitSnapshot, ProviderRateLimitWindow } from \"omk-ai\";\nimport type { AgentSession } from \"./agent-session.ts\";\n\nconst FIVE_HOUR_SECONDS = 5 * 60 * 60;\nconst SEVEN_DAY_SECONDS = 7 * 24 * 60 * 60;\nconst WINDOW_TOLERANCE_SECONDS = 120;\nconst CODEX_USAGE_URL = \"https://chatgpt.com/backend-api/wham/usage\";\nconst CLAUDE_USAGE_URL = \"https://api.anthropic.com/api/oauth/usage\";\nconst CLAUDE_MESSAGES_URL = \"https://api.anthropic.com/v1/messages\";\nconst KIMI_USAGE_URL = \"https://api.kimi.com/coding/v1/usages\";\nconst OPENAI_AUTH_CLAIM = \"https://api.openai.com/auth\";\nconst MAX_USAGE_RESPONSE_BYTES = 1024 * 1024;\nconst MAX_USAGE_LIMITS = 64;\nconst MAX_PASSIVE_ACCOUNTS = 64;\nconst PASSIVE_USAGE_TTL_MS = 6 * 60 * 60 * 1000;\nconst CLAUDE_QUOTA_PROBE_COOLDOWN_MS = 60 * 60 * 1000;\nconst CLAUDE_QUOTA_PROBE_TIMEOUT_MS = 10_000;\n\nexport type SubscriptionUsageWindow = {\n\treadonly label: string;\n\treadonly usedPercent: number;\n\treadonly resetsAt?: number;\n};\n\nexport type SubscriptionUsageSnapshot = {\n\treadonly label: string;\n\treadonly windows: readonly SubscriptionUsageWindow[];\n\treadonly message?: string;\n};\n\ntype CodexUsageWindow = { readonly usedPercent: number; readonly resetsAt?: number };\nexport type CodexUsageSnapshot = {\n\treadonly fiveHour?: CodexUsageWindow;\n\treadonly sevenDay?: CodexUsageWindow;\n};\n\ntype ParsedCodexWindow = CodexUsageWindow & { readonly windowSeconds?: number };\ntype ObservedCodexWindow = { readonly window: ParsedCodexWindow; readonly observedAt: number };\ntype PassiveUsageEntry = { readonly primary?: ObservedCodexWindow; readonly secondary?: ObservedCodexWindow };\ntype UsageKind = \"codex\" | \"claude\" | \"kimi\" | \"zai\" | \"unavailable\";\ntype CredentialCandidate = { readonly provider: string; readonly oauthOnly: boolean };\n\nexport type SubscriptionUsageSource = {\n\treadonly label: string;\n\treadonly kind: UsageKind;\n\treadonly credentials: readonly CredentialCandidate[];\n\treadonly ttlMs: number;\n\treadonly unavailableMessage?: string;\n};\n\ntype UsageSession = Pick<AgentSession, \"state\" | \"modelRegistry\">;\ntype FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\ntype FetchJsonResult = { readonly status: number; readonly payload?: unknown };\n\nconst passiveCodexUsage = new Map<string, PassiveUsageEntry>();\nconst passiveClaudeUsage = new Map<string, PassiveUsageEntry>();\nconst claudeQuotaProbeAttempts = new Map<string, number>();\nconst claudeQuotaProbesInFlight = new Map<string, Promise<void>>();\nconst subscriptionUsageRevisions = new Map<string, number>();\nconst MINUTE_TTL_MS = 60_000;\nconst VISIBLE_USAGE_PROVIDERS = [\n\t\"openai-codex\",\n\t\"anthropic\",\n\t\"kimi-coding\",\n\t\"zai\",\n\t\"modelstudio-maas\",\n\t\"qwen-oauth\",\n\t\"grok-oauth-proxy\",\n] as const;\nconst SOURCES: Readonly<Record<string, SubscriptionUsageSource>> = {\n\t\"openai-codex\": source(\"CODEX\", \"codex\", [{ provider: \"openai-codex\", oauthOnly: true }]),\n\tanthropic: source(\"CLAUDE\", \"claude\", [{ provider: \"anthropic\", oauthOnly: true }], 5 * MINUTE_TTL_MS),\n\t\"qwen-oauth\": source(\"QWEN\", \"unavailable\", [{ provider: \"qwen-oauth\", oauthOnly: true }]),\n\t\"modelstudio-maas\": source(\n\t\t\"QWEN TOKEN PLAN\",\n\t\t\"unavailable\",\n\t\t[{ provider: \"modelstudio-maas\", oauthOnly: false }],\n\t\tMINUTE_TTL_MS,\n\t\t\"console-only quota\",\n\t),\n\t\"kimi-code\": source(\"KIMI\", \"kimi\", [\n\t\t{ provider: \"kimi-code\", oauthOnly: true },\n\t\t{ provider: \"kimi-coding\", oauthOnly: false },\n\t]),\n\t\"kimi-coding\": source(\"KIMI\", \"kimi\", [\n\t\t{ provider: \"kimi-code\", oauthOnly: true },\n\t\t{ provider: \"kimi-coding\", oauthOnly: false },\n\t]),\n\t\"zhipu-coding-plan\": source(\"GLM\", \"zai\", [{ provider: \"zhipu-coding-plan\", oauthOnly: true }]),\n\tzai: source(\"GLM\", \"zai\", [\n\t\t{ provider: \"zai\", oauthOnly: false },\n\t\t{ provider: \"zai-coding-cn\", oauthOnly: false },\n\t\t{ provider: \"zhipu-coding-plan\", oauthOnly: true },\n\t]),\n\t\"zai-coding-cn\": source(\"GLM\", \"zai\", [\n\t\t{ provider: \"zai-coding-cn\", oauthOnly: false },\n\t\t{ provider: \"zhipu-coding-plan\", oauthOnly: true },\n\t]),\n\t\"grok-oauth-proxy\": source(\"GROK\", \"unavailable\", [\n\t\t{ provider: \"grok-oauth-proxy\", oauthOnly: true },\n\t\t{ provider: \"xai\", oauthOnly: true },\n\t]),\n\txai: source(\"GROK\", \"unavailable\", [{ provider: \"xai\", oauthOnly: true }]),\n};\n\nfunction source(\n\tlabel: string,\n\tkind: UsageKind,\n\tcredentials: readonly CredentialCandidate[],\n\tttlMs = MINUTE_TTL_MS,\n\tunavailableMessage?: string,\n): SubscriptionUsageSource {\n\treturn { label, kind, credentials, ttlMs, unavailableMessage };\n}\n\nexport function getSubscriptionUsageSource(provider: string | undefined): SubscriptionUsageSource | undefined {\n\tif (!provider) return undefined;\n\tconst configured = SOURCES[provider];\n\tif (configured) return configured;\n\tif (/^openai-codex-\\d+$/.test(provider)) {\n\t\treturn source(\"CODEX\", \"codex\", [\n\t\t\t{ provider, oauthOnly: true },\n\t\t\t{ provider: \"openai-codex\", oauthOnly: true },\n\t\t]);\n\t}\n\treturn undefined;\n}\n\nfunction usageRevisionKey(provider: string): string {\n\tconst kind = getSubscriptionUsageSource(provider)?.kind;\n\tif (kind === \"codex\") return \"codex\";\n\tif (kind === \"claude\") return \"claude\";\n\treturn provider;\n}\n\nexport function getSubscriptionUsageRevision(provider: string): number {\n\treturn subscriptionUsageRevisions.get(usageRevisionKey(provider)) ?? 0;\n}\n\nfunction bumpSubscriptionUsageRevision(provider: string): void {\n\tconst key = usageRevisionKey(provider);\n\tsubscriptionUsageRevisions.set(key, (subscriptionUsageRevisions.get(key) ?? 0) + 1);\n}\n\nexport function supportsSubscriptionUsage(session: UsageSession, provider = session.state.model?.provider): boolean {\n\tconst usageSource = getSubscriptionUsageSource(provider);\n\treturn usageSource?.credentials.some((candidate) => credentialConfigured(session, candidate)) ?? false;\n}\n\n/** Configured quota groups shown in the rail, with the active group first. */\nexport function getConfiguredSubscriptionUsageProviders(session: UsageSession): readonly string[] {\n\tconst activeProvider = session.state.model?.provider;\n\tconst activeSource = getSubscriptionUsageSource(activeProvider);\n\tconst providers = VISIBLE_USAGE_PROVIDERS.filter((provider) => supportsSubscriptionUsage(session, provider));\n\tif (!activeProvider || !activeSource || !supportsSubscriptionUsage(session, activeProvider)) return providers;\n\tconst activeCanonical = providers.find(\n\t\t(provider) => getSubscriptionUsageSource(provider)?.label === activeSource.label,\n\t);\n\tif (!activeCanonical) return [activeProvider, ...providers];\n\treturn [activeCanonical, ...providers.filter((provider) => provider !== activeCanonical)];\n}\n\nexport async function loadSubscriptionUsage(\n\tsession: UsageSession,\n\tfetchImpl: FetchLike = fetch,\n\tprovider = session.state.model?.provider,\n): Promise<SubscriptionUsageSnapshot | undefined> {\n\tconst model = session.state.model;\n\tconst usageSource = getSubscriptionUsageSource(provider);\n\tif (!model || !usageSource || !supportsSubscriptionUsage(session, provider)) return undefined;\n\tif (usageSource.kind === \"unavailable\") {\n\t\treturn {\n\t\t\tlabel: usageSource.label,\n\t\t\twindows: [],\n\t\t\tmessage: usageSource.unavailableMessage ?? \"quota API unavailable\",\n\t\t};\n\t}\n\tif (offline()) return { label: usageSource.label, windows: [], message: \"offline\" };\n\n\tconst credential = await resolveCredential(session, usageSource);\n\tif (!credential) return { label: usageSource.label, windows: [], message: \"usage unavailable\" };\n\n\ttry {\n\t\tswitch (usageSource.kind) {\n\t\t\tcase \"codex\":\n\t\t\t\treturn await fetchCodexUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"claude\":\n\t\t\t\treturn await fetchClaudeUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"kimi\":\n\t\t\t\treturn await fetchKimiUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"zai\":\n\t\t\t\treturn await fetchZaiUsage(usageSource.label, provider ?? model.provider, credential, fetchImpl);\n\t\t}\n\t} catch {\n\t\treturn { label: usageSource.label, windows: [], message: \"usage unavailable\" };\n\t}\n}\n\nexport function parseCodexUsageSnapshot(\n\tvalue: unknown,\n\tnowSeconds = Date.now() / 1000,\n): CodexUsageSnapshot | undefined {\n\tconst rateLimit = record(record(value)?.rate_limit);\n\tconst primary = codexWindow(rateLimit?.primary_window, nowSeconds);\n\tconst secondary = codexWindow(rateLimit?.secondary_window, nowSeconds);\n\tconst windows = [primary, secondary].filter((window): window is ParsedCodexWindow => window !== undefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tconst fallbackFiveHour = fiveHour ?? (primary?.windowSeconds === undefined ? primary : undefined);\n\tconst fallbackSevenDay = sevenDay ?? (secondary?.windowSeconds === undefined ? secondary : undefined);\n\tif (!fallbackFiveHour && !fallbackSevenDay) return undefined;\n\treturn {\n\t\t...(fallbackFiveHour ? { fiveHour: publicCodexWindow(fallbackFiveHour) } : {}),\n\t\t...(fallbackSevenDay ? { sevenDay: publicCodexWindow(fallbackSevenDay) } : {}),\n\t};\n}\n\nexport function parseClaudeUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tif (!payload) return undefined;\n\tconst entries = Array.isArray(payload.limits)\n\t\t? payload.limits.slice(0, MAX_USAGE_LIMITS).map(record).filter(isDefined)\n\t\t: [];\n\tconst fiveHour = claudeWindow(payload.five_hour) ?? claudeWindow(entries.find((entry) => entry.kind === \"session\"));\n\tconst sevenDay =\n\t\tclaudeWindow(payload.seven_day) ?? claudeWindow(entries.find((entry) => entry.kind === \"weekly_all\"));\n\tconst windows = [\n\t\tfiveHour ? withLabel(\"5H\", fiveHour) : undefined,\n\t\tsevenDay ? withLabel(\"7D\", sevenDay) : undefined,\n\t].filter(isDefined);\n\treturn windows.length > 0 ? windows : undefined;\n}\n\nexport function parseKimiUsageSnapshot(\n\tvalue: unknown,\n\tnowSeconds = Date.now() / 1000,\n): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tif (!payload) return undefined;\n\tconst windows: SubscriptionUsageWindow[] = [];\n\tconst total = usageRatio(record(payload.usage), nowSeconds);\n\tif (total) windows.push(withLabel(\"TOTAL\", total));\n\tif (Array.isArray(payload.limits)) {\n\t\tfor (const rawLimit of payload.limits.slice(0, MAX_USAGE_LIMITS)) {\n\t\t\tconst limit = record(rawLimit);\n\t\t\tif (!limit) continue;\n\t\t\tconst detail = record(limit.detail) ?? limit;\n\t\t\tconst windowData = record(limit.window);\n\t\t\tconst parsed = usageRatio(detail, nowSeconds);\n\t\t\tif (!parsed) continue;\n\t\t\tconst resetsAt = parseReset(windowData, nowSeconds) ?? parsed.resetsAt;\n\t\t\twindows.push({\n\t\t\t\tlabel: durationLabel(windowData) ?? shortLabel(limit.name ?? limit.title ?? limit.scope, windows.length),\n\t\t\t\tusedPercent: parsed.usedPercent,\n\t\t\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t\t\t});\n\t\t}\n\t}\n\treturn windows.length > 0 ? windows.slice(0, 4) : undefined;\n}\n\nexport function parseZaiUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tconst limits = record(payload?.data)?.limits;\n\tif (payload?.success !== true || !Array.isArray(limits)) return undefined;\n\tconst parsed = limits.slice(0, MAX_USAGE_LIMITS).map(zaiWindow).filter(isDefined);\n\tconst requestWindows = parsed.filter((window) => window.type === \"TIME_LIMIT\" && !window.featureOnly);\n\tconst selected =\n\t\trequestWindows.length > 0 ? requestWindows : parsed.filter((window) => window.type === \"TOKENS_LIMIT\");\n\tselected.sort((left, right) => left.durationSeconds - right.durationSeconds);\n\tconst windows = selected\n\t\t.slice(0, 4)\n\t\t.map(({ type: _type, durationSeconds: _duration, featureOnly: _feature, ...window }) => window);\n\treturn windows.length > 0 ? windows : undefined;\n}\n\nexport function recordCodexPassiveUsage(apiKey: string, snapshot: ProviderRateLimitSnapshot, nowMs = Date.now()): void {\n\tconst accountId = codexAccountId(apiKey);\n\tconst limitId = snapshot.limitId?.trim().toLowerCase().replace(/-/g, \"_\");\n\tif (!accountId || !Number.isFinite(nowMs) || nowMs < 0 || (limitId && limitId !== \"codex\")) return;\n\tprunePassiveCodexUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(accountId);\n\tconst primary = normalizePassiveCodexWindow(snapshot.primary, nowMs);\n\tconst secondary = normalizePassiveCodexWindow(snapshot.secondary, nowMs);\n\tif (!primary && !secondary) return;\n\n\tconst previous = passiveCodexUsage.get(cacheKey);\n\tconst next: PassiveUsageEntry = {\n\t\tprimary: primary ? { window: primary, observedAt: nowMs } : freshObservedCodexWindow(previous?.primary, nowMs),\n\t\tsecondary: secondary\n\t\t\t? { window: secondary, observedAt: nowMs }\n\t\t\t: freshObservedCodexWindow(previous?.secondary, nowMs),\n\t};\n\tpassiveCodexUsage.delete(cacheKey);\n\tpassiveCodexUsage.set(cacheKey, next);\n\twhile (passiveCodexUsage.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = passiveCodexUsage.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tpassiveCodexUsage.delete(oldestCacheKey);\n\t}\n\tbumpSubscriptionUsageRevision(\"openai-codex\");\n}\n\nexport function recordClaudePassiveUsage(\n\tapiKey: string,\n\tsnapshot: ProviderRateLimitSnapshot,\n\tnowMs = Date.now(),\n): void {\n\tconst limitId = snapshot.limitId?.trim().toLowerCase().replace(/-/g, \"_\");\n\tif (\n\t\t!apiKey ||\n\t\tapiKey.length > 32_768 ||\n\t\t!Number.isFinite(nowMs) ||\n\t\tnowMs < 0 ||\n\t\t(limitId && limitId !== \"anthropic_unified\")\n\t) {\n\t\treturn;\n\t}\n\tprunePassiveClaudeUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst primary = normalizePassiveCodexWindow(snapshot.primary, nowMs);\n\tconst secondary = normalizePassiveCodexWindow(snapshot.secondary, nowMs);\n\tif (!primary && !secondary) return;\n\n\tconst previous = passiveClaudeUsage.get(cacheKey);\n\tconst next: PassiveUsageEntry = {\n\t\tprimary: primary ? { window: primary, observedAt: nowMs } : freshObservedCodexWindow(previous?.primary, nowMs),\n\t\tsecondary: secondary\n\t\t\t? { window: secondary, observedAt: nowMs }\n\t\t\t: freshObservedCodexWindow(previous?.secondary, nowMs),\n\t};\n\tpassiveClaudeUsage.delete(cacheKey);\n\tpassiveClaudeUsage.set(cacheKey, next);\n\twhile (passiveClaudeUsage.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = passiveClaudeUsage.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tpassiveClaudeUsage.delete(oldestCacheKey);\n\t}\n\tbumpSubscriptionUsageRevision(\"anthropic\");\n}\n\nasync function fetchCodexUsage(\n\tlabel: string,\n\tapiKey: string,\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tconst accountId = codexAccountId(apiKey);\n\tif (!accountId) return { label, windows: [], message: \"usage unavailable\" };\n\tconst passive = passiveCodexSnapshot(apiKey);\n\tlet polled: CodexUsageSnapshot | undefined;\n\ttry {\n\t\tconst payload = await fetchJson(fetchImpl, CODEX_USAGE_URL, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"chatgpt-account-id\": accountId,\n\t\t\t\t\"User-Agent\": \"omk\",\n\t\t\t},\n\t\t});\n\t\tpolled = parseCodexUsageSnapshot(payload);\n\t} catch (error) {\n\t\tif (!passive) throw error;\n\t}\n\tconst merged: CodexUsageSnapshot = {\n\t\tfiveHour: polled?.fiveHour ?? passive?.fiveHour,\n\t\tsevenDay: polled?.sevenDay ?? passive?.sevenDay,\n\t};\n\tif (!merged.fiveHour && !merged.sevenDay) return { label, windows: [], message: \"usage unavailable\" };\n\treturn {\n\t\tlabel,\n\t\twindows: [\n\t\t\tmerged.fiveHour ? withLabel(\"5H\", merged.fiveHour) : undefined,\n\t\t\tmerged.sevenDay ? withLabel(\"7D\", merged.sevenDay) : undefined,\n\t\t].filter(isDefined),\n\t};\n}\n\nasync function fetchClaudeUsage(\n\tlabel: string,\n\tapiKey: string,\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tlet passive = passiveClaudeSnapshot(apiKey);\n\tconst response = await fetchJsonResult(fetchImpl, CLAUDE_USAGE_URL, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"anthropic-beta\": \"claude-code-20250219,oauth-2025-04-20\",\n\t\t\t\"User-Agent\": \"claude-cli/2.1.177 (external, cli)\",\n\t\t},\n\t});\n\tconst windows = [...(parseClaudeUsageSnapshot(response.payload) ?? [])];\n\tif (response.status === 429 && (!passive?.fiveHour || !passive?.sevenDay)) {\n\t\tawait probeClaudeQuota(apiKey, fetchImpl);\n\t\tpassive = passiveClaudeSnapshot(apiKey);\n\t}\n\tfor (const passiveWindow of [\n\t\tpassive?.fiveHour ? withLabel(\"5H\", passive.fiveHour) : undefined,\n\t\tpassive?.sevenDay ? withLabel(\"7D\", passive.sevenDay) : undefined,\n\t].filter(isDefined)) {\n\t\tif (!windows.some((window) => window.label === passiveWindow.label)) windows.push(passiveWindow);\n\t}\n\treturn windows.length > 0\n\t\t? { label, windows }\n\t\t: { label, windows: [], message: response.status === 429 ? \"rate limited · retry later\" : \"usage unavailable\" };\n}\n\n/**\n * Claude Code 2.1.177 performs the same `quota`/`max_tokens: 1` request during startup.\n * Keep it account-scoped and hourly so a pinned sidebar cannot turn the fallback into polling.\n */\nasync function probeClaudeQuota(apiKey: string, fetchImpl: FetchLike, nowMs = Date.now()): Promise<void> {\n\tif (!apiKey || apiKey.length > 32_768 || !Number.isFinite(nowMs) || nowMs < 0) return;\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst activeProbe = claudeQuotaProbesInFlight.get(cacheKey);\n\tif (activeProbe) return activeProbe;\n\tconst lastAttempt = claudeQuotaProbeAttempts.get(cacheKey);\n\tif (lastAttempt !== undefined && nowMs - lastAttempt < CLAUDE_QUOTA_PROBE_COOLDOWN_MS) return;\n\tif (claudeQuotaProbesInFlight.size >= MAX_PASSIVE_ACCOUNTS) return;\n\n\tclaudeQuotaProbeAttempts.delete(cacheKey);\n\tclaudeQuotaProbeAttempts.set(cacheKey, nowMs);\n\twhile (claudeQuotaProbeAttempts.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = claudeQuotaProbeAttempts.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tclaudeQuotaProbeAttempts.delete(oldestCacheKey);\n\t}\n\n\tconst probe = (async () => {\n\t\ttry {\n\t\t\tconst response = await fetchImpl(CLAUDE_MESSAGES_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\t\"anthropic-beta\": \"claude-code-20250219,oauth-2025-04-20\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"User-Agent\": \"claude-cli/2.1.177 (external, cli)\",\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: \"claude-haiku-4-5\",\n\t\t\t\t\tmax_tokens: 1,\n\t\t\t\t\tmessages: [{ role: \"user\", content: \"quota\" }],\n\t\t\t\t}),\n\t\t\t\tsignal: AbortSignal.timeout(CLAUDE_QUOTA_PROBE_TIMEOUT_MS),\n\t\t\t});\n\t\t\tconst snapshot = parseClaudeQuotaProbeHeaders(response.headers, nowMs);\n\t\t\tif (snapshot) recordClaudePassiveUsage(apiKey, snapshot, nowMs);\n\t\t\ttry {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t} catch {\n\t\t\t\t// The quota snapshot lives entirely in response headers.\n\t\t\t}\n\t\t} catch {\n\t\t\t// This mirrors Claude Code's best-effort startup quota check.\n\t\t}\n\t})().finally(() => {\n\t\tclaudeQuotaProbesInFlight.delete(cacheKey);\n\t});\n\tclaudeQuotaProbesInFlight.set(cacheKey, probe);\n\treturn probe;\n}\n\nfunction parseClaudeQuotaProbeHeaders(headers: Headers, nowMs: number): ProviderRateLimitSnapshot | undefined {\n\tconst parseWindow = (prefix: \"5h\" | \"7d\", windowSeconds: number): ProviderRateLimitWindow | undefined => {\n\t\tconst rawUtilization = headers.get(`anthropic-ratelimit-unified-${prefix}-utilization`);\n\t\tconst rawReset = headers.get(`anthropic-ratelimit-unified-${prefix}-reset`);\n\t\tif (rawUtilization === null || rawReset === null) return undefined;\n\t\tconst utilization = Number(rawUtilization);\n\t\tconst resetsAt = Number(rawReset);\n\t\tconst maxResetAt = nowMs / 1000 + 10 * 365 * 24 * 60 * 60;\n\t\tif (\n\t\t\t!Number.isFinite(utilization) ||\n\t\t\tutilization < 0 ||\n\t\t\tutilization > 1 ||\n\t\t\t!Number.isSafeInteger(resetsAt) ||\n\t\t\tresetsAt <= 0 ||\n\t\t\tresetsAt > maxResetAt\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn { usedPercent: Math.round(utilization * 10_000) / 100, windowSeconds, resetsAt };\n\t};\n\tconst primary = parseWindow(\"5h\", FIVE_HOUR_SECONDS);\n\tconst secondary = parseWindow(\"7d\", SEVEN_DAY_SECONDS);\n\treturn primary || secondary ? { limitId: \"anthropic-unified\", primary, secondary } : undefined;\n}\n\nasync function fetchKimiUsage(label: string, apiKey: string, fetchImpl: FetchLike): Promise<SubscriptionUsageSnapshot> {\n\tconst payload = await fetchJson(fetchImpl, KIMI_USAGE_URL, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": \"KimiCLI/1.5\",\n\t\t\t\"X-Msh-Platform\": \"kimi_cli\",\n\t\t\t\"X-Msh-Version\": \"1.5\",\n\t\t},\n\t});\n\tconst windows = parseKimiUsageSnapshot(payload);\n\treturn windows ? { label, windows } : { label, windows: [], message: \"usage unavailable\" };\n}\n\nasync function fetchZaiUsage(\n\tlabel: string,\n\tmodelProvider: string,\n\tcredential: { readonly provider: string; readonly apiKey: string },\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tconst origin =\n\t\tmodelProvider === \"zai-coding-cn\" ||\n\t\tcredential.provider === \"zai-coding-cn\" ||\n\t\tcredential.provider === \"zhipu-coding-plan\"\n\t\t\t? \"https://open.bigmodel.cn\"\n\t\t\t: \"https://api.z.ai\";\n\tconst payload = await fetchJson(fetchImpl, `${origin}/api/monitor/usage/quota/limit`, {\n\t\theaders: {\n\t\t\tAuthorization: credential.apiKey,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": \"omk\",\n\t\t},\n\t});\n\tconst windows = parseZaiUsageSnapshot(payload);\n\treturn windows ? { label, windows } : { label, windows: [], message: \"usage unavailable\" };\n}\n\nasync function fetchJson(fetchImpl: FetchLike, url: string, init: RequestInit): Promise<unknown> {\n\treturn (await fetchJsonResult(fetchImpl, url, init)).payload;\n}\n\nasync function fetchJsonResult(fetchImpl: FetchLike, url: string, init: RequestInit): Promise<FetchJsonResult> {\n\tconst response = await fetchImpl(url, { ...init, signal: AbortSignal.timeout(10_000) });\n\tif (!response.ok) return { status: response.status };\n\tconst declaredBytes = Number(response.headers?.get(\"content-length\"));\n\tif (Number.isFinite(declaredBytes) && declaredBytes > MAX_USAGE_RESPONSE_BYTES) {\n\t\treturn { status: response.status };\n\t}\n\tif (!response.body || typeof response.body.getReader !== \"function\") {\n\t\treturn { status: response.status, payload: await response.json() };\n\t}\n\n\tconst reader = response.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet totalBytes = 0;\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotalBytes += value.byteLength;\n\t\tif (totalBytes > MAX_USAGE_RESPONSE_BYTES) {\n\t\t\tawait reader.cancel();\n\t\t\treturn { status: response.status };\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst body = new Uint8Array(totalBytes);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbody.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\ttry {\n\t\treturn { status: response.status, payload: JSON.parse(new TextDecoder().decode(body)) as unknown };\n\t} catch {\n\t\treturn { status: response.status };\n\t}\n}\n\nfunction credentialConfigured(session: UsageSession, candidate: CredentialCandidate): boolean {\n\tif (candidate.oauthOnly) return session.modelRegistry.isUsingOAuthProvider(candidate.provider);\n\tconst status = session.modelRegistry.getProviderAuthStatus(candidate.provider);\n\treturn status.configured || status.source !== undefined;\n}\n\nasync function resolveCredential(\n\tsession: UsageSession,\n\tusageSource: SubscriptionUsageSource,\n): Promise<{ readonly provider: string; readonly apiKey: string } | undefined> {\n\tfor (const candidate of usageSource.credentials) {\n\t\tif (!credentialConfigured(session, candidate)) continue;\n\t\tconst apiKey = await session.modelRegistry.getApiKeyForProvider(candidate.provider);\n\t\tif (apiKey) return { provider: candidate.provider, apiKey };\n\t}\n\treturn undefined;\n}\n\nfunction codexWindow(value: unknown, nowSeconds: number): ParsedCodexWindow | undefined {\n\tconst window = record(value);\n\tconst usedPercent = finiteNumber(window?.used_percent);\n\tif (usedPercent === undefined) return undefined;\n\tconst windowSeconds = finiteNumber(window?.limit_window_seconds);\n\tconst resetAt = finiteNumber(window?.reset_at);\n\tconst resetAfter = finiteNumber(window?.reset_after_seconds);\n\tconst resetsAt = resetAt ?? (resetAfter === undefined ? undefined : nowSeconds + resetAfter);\n\treturn {\n\t\tusedPercent: clampPercent(usedPercent),\n\t\t...(windowSeconds === undefined ? {} : { windowSeconds }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction claudeWindow(value: unknown): Omit<SubscriptionUsageWindow, \"label\"> | undefined {\n\tconst bucket = record(value);\n\tif (!bucket || bucket.is_active === false) return undefined;\n\tconst usedPercent = finiteNumber(bucket.utilization ?? bucket.percent);\n\tif (usedPercent === undefined) return undefined;\n\tconst resetsAt = epochSeconds(bucket.resets_at);\n\treturn { usedPercent: clampPercent(usedPercent), ...(resetsAt === undefined ? {} : { resetsAt }) };\n}\n\nfunction usageRatio(\n\tdata: Record<string, unknown> | undefined,\n\tnowSeconds: number,\n): Omit<SubscriptionUsageWindow, \"label\"> | undefined {\n\tif (!data) return undefined;\n\tconst limit = finiteNumber(data.limit);\n\tlet used = finiteNumber(data.used);\n\tconst remaining = finiteNumber(data.remaining);\n\tif (used === undefined && limit !== undefined && remaining !== undefined) used = limit - remaining;\n\tif (used === undefined || limit === undefined || limit <= 0) return undefined;\n\tconst resetsAt = parseReset(data, nowSeconds);\n\treturn {\n\t\tusedPercent: clampPercent((used / limit) * 100),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction zaiWindow(value: unknown):\n\t| (SubscriptionUsageWindow & {\n\t\t\treadonly type: string;\n\t\t\treadonly durationSeconds: number;\n\t\t\treadonly featureOnly: boolean;\n\t })\n\t| undefined {\n\tconst item = record(value);\n\tif (!item || (item.type !== \"TIME_LIMIT\" && item.type !== \"TOKENS_LIMIT\")) return undefined;\n\tconst percentage = finiteNumber(item.percentage);\n\tconst current = finiteNumber(item.currentValue);\n\tconst limit = finiteNumber(item.usage);\n\tconst usedPercent =\n\t\tpercentage ?? (current !== undefined && limit !== undefined && limit > 0 ? (current / limit) * 100 : undefined);\n\tif (usedPercent === undefined) return undefined;\n\tconst duration = zaiDuration(finiteNumber(item.unit), finiteNumber(item.number));\n\tconst resetsAt = epochSeconds(item.nextResetTime);\n\tconst detailCodes = Array.isArray(item.usageDetails)\n\t\t? item.usageDetails\n\t\t\t\t.map((detail) => record(detail)?.modelCode)\n\t\t\t\t.filter((code): code is string => typeof code === \"string\")\n\t\t: [];\n\treturn {\n\t\ttype: item.type,\n\t\tlabel: duration.label,\n\t\tdurationSeconds: duration.seconds,\n\t\tfeatureOnly: [\"search-prime\", \"web-reader\", \"zread\"].every((code) => detailCodes.includes(code)),\n\t\tusedPercent: clampPercent(usedPercent),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction zaiDuration(unit: number | undefined, countValue: number | undefined): { label: string; seconds: number } {\n\tconst count = countValue && countValue > 0 ? countValue : 1;\n\tif (unit === 3) return { label: `${count}H`, seconds: count * 60 * 60 };\n\tif (unit === 4) return { label: `${count}D`, seconds: count * 24 * 60 * 60 };\n\tif (unit === 5) return { label: count === 1 ? \"30D\" : `${count}MO`, seconds: count * 30 * 24 * 60 * 60 };\n\tif (unit === 6) return { label: \"7D\", seconds: 7 * 24 * 60 * 60 };\n\treturn { label: \"QUOTA\", seconds: Number.POSITIVE_INFINITY };\n}\n\nfunction durationLabel(window: Record<string, unknown> | undefined): string | undefined {\n\tif (!window) return undefined;\n\tconst duration = finiteNumber(window.duration);\n\tconst unit = typeof window.timeUnit === \"string\" ? window.timeUnit.toUpperCase() : \"\";\n\tif (duration === undefined) return undefined;\n\tif (unit.includes(\"MINUTE\")) return duration % 60 === 0 ? `${duration / 60}H` : `${duration}M`;\n\tif (unit.includes(\"HOUR\")) return `${duration}H`;\n\tif (unit.includes(\"DAY\")) return `${duration}D`;\n\tif (unit.includes(\"SECOND\")) return `${duration}S`;\n\treturn undefined;\n}\n\nfunction parseReset(data: Record<string, unknown> | undefined, nowSeconds: number): number | undefined {\n\tif (!data) return undefined;\n\tfor (const key of [\"reset_at\", \"resetAt\", \"reset_time\", \"resetTime\"] as const) {\n\t\tconst parsed = epochSeconds(data[key]);\n\t\tif (parsed !== undefined) return parsed;\n\t}\n\tfor (const key of [\"reset_in\", \"resetIn\", \"ttl\"] as const) {\n\t\tconst seconds = finiteNumber(data[key]);\n\t\tif (seconds !== undefined) return nowSeconds + seconds;\n\t}\n\treturn undefined;\n}\n\nfunction epochSeconds(value: unknown): number | undefined {\n\tconst numeric = finiteNumber(value);\n\tif (numeric !== undefined) return numeric > 1_000_000_000_000 ? numeric / 1000 : numeric;\n\tif (typeof value !== \"string\" || !value.trim()) return undefined;\n\tconst parsed = Date.parse(value);\n\treturn Number.isFinite(parsed) ? parsed / 1000 : undefined;\n}\n\nfunction normalizePassiveCodexWindow(\n\twindow: ProviderRateLimitWindow | undefined,\n\tnowMs: number,\n): ParsedCodexWindow | undefined {\n\tif (!window || !Number.isFinite(window.usedPercent)) return undefined;\n\tconst windowSeconds =\n\t\twindow.windowSeconds !== undefined &&\n\t\tNumber.isFinite(window.windowSeconds) &&\n\t\twindow.windowSeconds > 0 &&\n\t\twindow.windowSeconds <= Number.MAX_SAFE_INTEGER\n\t\t\t? Math.round(window.windowSeconds)\n\t\t\t: undefined;\n\tconst maxResetAt = nowMs / 1000 + 10 * 365 * 24 * 60 * 60;\n\tconst resetsAtValue = window.resetsAt === undefined ? undefined : Math.floor(window.resetsAt);\n\tconst resetsAt =\n\t\tresetsAtValue !== undefined &&\n\t\tNumber.isSafeInteger(resetsAtValue) &&\n\t\tresetsAtValue > 0 &&\n\t\tresetsAtValue <= maxResetAt\n\t\t\t? resetsAtValue\n\t\t\t: undefined;\n\treturn {\n\t\tusedPercent: clampPercent(window.usedPercent),\n\t\t...(windowSeconds === undefined ? {} : { windowSeconds }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction passiveCredentialCacheKey(identifier: string): string {\n\treturn createHash(\"sha256\").update(identifier).digest(\"base64url\");\n}\n\nfunction freshObservedCodexWindow(\n\tobserved: ObservedCodexWindow | undefined,\n\tnowMs: number,\n): ObservedCodexWindow | undefined {\n\tif (!observed || nowMs - observed.observedAt > PASSIVE_USAGE_TTL_MS) return undefined;\n\tif (observed.window.resetsAt !== undefined && observed.window.resetsAt <= nowMs / 1000) return undefined;\n\treturn observed;\n}\n\nfunction prunePassiveCodexUsage(nowMs: number): void {\n\tfor (const [cacheKey, entry] of passiveCodexUsage) {\n\t\tif (!freshObservedCodexWindow(entry.primary, nowMs) && !freshObservedCodexWindow(entry.secondary, nowMs)) {\n\t\t\tpassiveCodexUsage.delete(cacheKey);\n\t\t}\n\t}\n}\n\nfunction prunePassiveClaudeUsage(nowMs: number): void {\n\tfor (const [cacheKey, entry] of passiveClaudeUsage) {\n\t\tif (!freshObservedCodexWindow(entry.primary, nowMs) && !freshObservedCodexWindow(entry.secondary, nowMs)) {\n\t\t\tpassiveClaudeUsage.delete(cacheKey);\n\t\t}\n\t}\n}\n\nfunction passiveCodexSnapshot(apiKey: string, nowMs = Date.now()): CodexUsageSnapshot | undefined {\n\tconst accountId = codexAccountId(apiKey);\n\tif (!accountId || !Number.isFinite(nowMs) || nowMs < 0) return undefined;\n\tprunePassiveCodexUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(accountId);\n\tconst entry = passiveCodexUsage.get(cacheKey);\n\tif (!entry) return undefined;\n\tconst primary = freshObservedCodexWindow(entry.primary, nowMs);\n\tconst secondary = freshObservedCodexWindow(entry.secondary, nowMs);\n\tif (!primary && !secondary) {\n\t\tpassiveCodexUsage.delete(cacheKey);\n\t\treturn undefined;\n\t}\n\tconst freshEntry = { primary, secondary };\n\tpassiveCodexUsage.delete(cacheKey);\n\tpassiveCodexUsage.set(cacheKey, freshEntry);\n\n\tconst windows = [primary?.window, secondary?.window].filter(isDefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tif (!fiveHour && !sevenDay) return undefined;\n\treturn {\n\t\t...(fiveHour ? { fiveHour: publicCodexWindow(fiveHour) } : {}),\n\t\t...(sevenDay ? { sevenDay: publicCodexWindow(sevenDay) } : {}),\n\t};\n}\n\nfunction passiveClaudeSnapshot(apiKey: string, nowMs = Date.now()): CodexUsageSnapshot | undefined {\n\tif (!apiKey || apiKey.length > 32_768 || !Number.isFinite(nowMs) || nowMs < 0) return undefined;\n\tprunePassiveClaudeUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst entry = passiveClaudeUsage.get(cacheKey);\n\tif (!entry) return undefined;\n\tconst primary = freshObservedCodexWindow(entry.primary, nowMs);\n\tconst secondary = freshObservedCodexWindow(entry.secondary, nowMs);\n\tif (!primary && !secondary) {\n\t\tpassiveClaudeUsage.delete(cacheKey);\n\t\treturn undefined;\n\t}\n\tpassiveClaudeUsage.delete(cacheKey);\n\tpassiveClaudeUsage.set(cacheKey, { primary, secondary });\n\n\tconst windows = [primary?.window, secondary?.window].filter(isDefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tif (!fiveHour && !sevenDay) return undefined;\n\treturn {\n\t\t...(fiveHour ? { fiveHour: publicCodexWindow(fiveHour) } : {}),\n\t\t...(sevenDay ? { sevenDay: publicCodexWindow(sevenDay) } : {}),\n\t};\n}\n\nfunction codexAccountId(token: string): string | undefined {\n\ttry {\n\t\tconst payload = token.split(\".\")[1];\n\t\tif (!payload) return undefined;\n\t\tconst claims = record(JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")));\n\t\tconst auth = record(claims?.[OPENAI_AUTH_CLAIM]);\n\t\tconst accountId = typeof auth?.chatgpt_account_id === \"string\" ? auth.chatgpt_account_id.trim() : \"\";\n\t\treturn accountId && accountId.length <= 256 ? accountId : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction publicCodexWindow(window: ParsedCodexWindow): CodexUsageWindow {\n\treturn { usedPercent: window.usedPercent, ...(window.resetsAt === undefined ? {} : { resetsAt: window.resetsAt }) };\n}\n\nfunction withLabel(label: string, window: Omit<SubscriptionUsageWindow, \"label\">): SubscriptionUsageWindow {\n\treturn { label, ...window };\n}\n\nfunction shortLabel(value: unknown, index: number): string {\n\tif (typeof value !== \"string\") return `LIMIT${index + 1}`;\n\tconst safe = value\n\t\t.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u206f]/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\treturn safe ? safe.toUpperCase().slice(0, 8) : `LIMIT${index + 1}`;\n}\n\nfunction finiteNumber(value: unknown): number | undefined {\n\tconst number =\n\t\ttypeof value === \"number\" ? value : typeof value === \"string\" && value.trim() ? Number(value) : Number.NaN;\n\treturn Number.isFinite(number) ? number : undefined;\n}\n\nfunction clampPercent(value: number): number {\n\treturn Number(Math.max(0, Math.min(100, value)).toFixed(2));\n}\n\nfunction near(actual: number | undefined, expected: number): boolean {\n\treturn actual !== undefined && Math.abs(actual - expected) <= WINDOW_TOLERANCE_SECONDS;\n}\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: undefined;\n}\n\nfunction isDefined<T>(value: T | undefined): value is T {\n\treturn value !== undefined;\n}\n\nfunction offline(): boolean {\n\tconst value = process.env.OMK_OFFLINE?.toLowerCase();\n\treturn value === \"1\" || value === \"true\" || value === \"yes\";\n}\n"]}
1
+ {"version":3,"file":"provider-usage.d.ts","sourceRoot":"","sources":["../../src/core/provider-usage.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport type { ProviderRateLimitSnapshot, ProviderRateLimitWindow } from \"omk-ai\";\nimport type { AgentSession } from \"./agent-session.ts\";\n\nconst FIVE_HOUR_SECONDS = 5 * 60 * 60;\nconst SEVEN_DAY_SECONDS = 7 * 24 * 60 * 60;\nconst WINDOW_TOLERANCE_SECONDS = 120;\nconst CODEX_USAGE_URL = \"https://chatgpt.com/backend-api/wham/usage\";\nconst CLAUDE_USAGE_URL = \"https://api.anthropic.com/api/oauth/usage\";\nconst CLAUDE_MESSAGES_URL = \"https://api.anthropic.com/v1/messages\";\nconst KIMI_USAGE_URL = \"https://api.kimi.com/coding/v1/usages\";\nconst GROK_BILLING_URL = \"https://cli-chat-proxy.grok.com/v1/billing?format=credits\";\nconst OPENAI_AUTH_CLAIM = \"https://api.openai.com/auth\";\nconst MAX_USAGE_RESPONSE_BYTES = 1024 * 1024;\nconst MAX_USAGE_LIMITS = 64;\nconst MAX_PASSIVE_ACCOUNTS = 64;\nconst PASSIVE_USAGE_TTL_MS = 6 * 60 * 60 * 1000;\nconst CLAUDE_QUOTA_PROBE_COOLDOWN_MS = 60 * 60 * 1000;\nconst CLAUDE_QUOTA_PROBE_TIMEOUT_MS = 10_000;\nconst QWEN_CLI_TIMEOUT_MS = 15_000;\nconst QWEN_CLI_ARGS = [\"usage\", \"summary\", \"--format\", \"json\"] as const;\nconst QWEN_CONNECT_HINT = \"connect: npm i -g @qwencloud/qwencloud-cli && qwencloud auth login\";\nconst QWEN_CLI_ENV_KEYS = [\n\t\"PATH\",\n\t\"Path\",\n\t\"HOME\",\n\t\"USERPROFILE\",\n\t\"HOMEDRIVE\",\n\t\"HOMEPATH\",\n\t\"APPDATA\",\n\t\"LOCALAPPDATA\",\n\t\"XDG_CONFIG_HOME\",\n\t\"XDG_DATA_HOME\",\n\t\"XDG_CACHE_HOME\",\n\t\"TMPDIR\",\n\t\"TEMP\",\n\t\"TMP\",\n\t\"SystemRoot\",\n\t\"WINDIR\",\n\t\"ComSpec\",\n\t\"PATHEXT\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TERM\",\n\t\"SSL_CERT_FILE\",\n\t\"SSL_CERT_DIR\",\n] as const;\n\nexport type SubscriptionUsageWindow = {\n\treadonly label: string;\n\treadonly usedPercent: number;\n\treadonly resetsAt?: number;\n};\n\nexport type SubscriptionUsageSnapshot = {\n\treadonly label: string;\n\treadonly windows: readonly SubscriptionUsageWindow[];\n\treadonly message?: string;\n};\n\ntype CodexUsageWindow = { readonly usedPercent: number; readonly resetsAt?: number };\nexport type CodexUsageSnapshot = {\n\treadonly fiveHour?: CodexUsageWindow;\n\treadonly sevenDay?: CodexUsageWindow;\n};\n\ntype ParsedCodexWindow = CodexUsageWindow & { readonly windowSeconds?: number };\ntype ObservedCodexWindow = { readonly window: ParsedCodexWindow; readonly observedAt: number };\ntype PassiveUsageEntry = { readonly primary?: ObservedCodexWindow; readonly secondary?: ObservedCodexWindow };\ntype UsageKind = \"codex\" | \"claude\" | \"kimi\" | \"zai\" | \"grok\" | \"qwen-token-plan\" | \"unavailable\";\ntype CredentialCandidate = { readonly provider: string; readonly oauthOnly: boolean };\n\nexport type SubscriptionUsageSource = {\n\treadonly label: string;\n\treadonly kind: UsageKind;\n\treadonly credentials: readonly CredentialCandidate[];\n\treadonly ttlMs: number;\n\treadonly unavailableMessage?: string;\n};\n\ntype UsageSession = Pick<AgentSession, \"state\" | \"modelRegistry\">;\ntype FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\ntype FetchJsonResult = { readonly status: number; readonly payload?: unknown };\n\nconst passiveCodexUsage = new Map<string, PassiveUsageEntry>();\nconst passiveClaudeUsage = new Map<string, PassiveUsageEntry>();\nconst claudeQuotaProbeAttempts = new Map<string, number>();\nconst claudeQuotaProbesInFlight = new Map<string, Promise<void>>();\nconst subscriptionUsageRevisions = new Map<string, number>();\nconst MINUTE_TTL_MS = 60_000;\nconst VISIBLE_USAGE_PROVIDERS = [\n\t\"openai-codex\",\n\t\"anthropic\",\n\t\"kimi-coding\",\n\t\"zai\",\n\t\"modelstudio-maas\",\n\t\"qwen-oauth\",\n\t\"xai\",\n] as const;\nconst SOURCES: Readonly<Record<string, SubscriptionUsageSource>> = {\n\t\"openai-codex\": source(\"CODEX\", \"codex\", [{ provider: \"openai-codex\", oauthOnly: true }]),\n\tanthropic: source(\"CLAUDE\", \"claude\", [{ provider: \"anthropic\", oauthOnly: true }], 5 * MINUTE_TTL_MS),\n\t\"qwen-oauth\": source(\"QWEN\", \"unavailable\", [{ provider: \"qwen-oauth\", oauthOnly: true }]),\n\t// Model Studio exposes no API-key quota endpoint (console gateway is\n\t// session-only), so the token-plan quota rides the official QwenCloud\n\t// management CLI, which holds its own OAuth credential.\n\t\"modelstudio-maas\": source(\n\t\t\"QWEN TOKEN PLAN\",\n\t\t\"qwen-token-plan\",\n\t\t[{ provider: \"modelstudio-maas\", oauthOnly: false }],\n\t\t5 * MINUTE_TTL_MS,\n\t),\n\t\"kimi-code\": source(\"KIMI\", \"kimi\", [\n\t\t{ provider: \"kimi-code\", oauthOnly: true },\n\t\t{ provider: \"kimi-coding\", oauthOnly: false },\n\t]),\n\t\"kimi-coding\": source(\"KIMI\", \"kimi\", [\n\t\t{ provider: \"kimi-code\", oauthOnly: true },\n\t\t{ provider: \"kimi-coding\", oauthOnly: false },\n\t]),\n\t\"zhipu-coding-plan\": source(\"GLM\", \"zai\", [{ provider: \"zhipu-coding-plan\", oauthOnly: true }]),\n\tzai: source(\"GLM\", \"zai\", [\n\t\t{ provider: \"zai\", oauthOnly: false },\n\t\t{ provider: \"zai-coding-cn\", oauthOnly: false },\n\t\t{ provider: \"zhipu-coding-plan\", oauthOnly: true },\n\t]),\n\t\"zai-coding-cn\": source(\"GLM\", \"zai\", [\n\t\t{ provider: \"zai-coding-cn\", oauthOnly: false },\n\t\t{ provider: \"zhipu-coding-plan\", oauthOnly: true },\n\t]),\n\txai: source(\"GROK\", \"grok\", [{ provider: \"xai\", oauthOnly: true }]),\n};\n\nfunction source(\n\tlabel: string,\n\tkind: UsageKind,\n\tcredentials: readonly CredentialCandidate[],\n\tttlMs = MINUTE_TTL_MS,\n\tunavailableMessage?: string,\n): SubscriptionUsageSource {\n\treturn { label, kind, credentials, ttlMs, unavailableMessage };\n}\n\nexport function getSubscriptionUsageSource(provider: string | undefined): SubscriptionUsageSource | undefined {\n\tif (!provider) return undefined;\n\tconst configured = SOURCES[provider];\n\tif (configured) return configured;\n\tif (/^openai-codex-\\d+$/.test(provider)) {\n\t\treturn source(\"CODEX\", \"codex\", [\n\t\t\t{ provider, oauthOnly: true },\n\t\t\t{ provider: \"openai-codex\", oauthOnly: true },\n\t\t]);\n\t}\n\treturn undefined;\n}\n\nfunction usageRevisionKey(provider: string): string {\n\tconst kind = getSubscriptionUsageSource(provider)?.kind;\n\tif (kind === \"codex\") return \"codex\";\n\tif (kind === \"claude\") return \"claude\";\n\treturn provider;\n}\n\nexport function getSubscriptionUsageRevision(provider: string): number {\n\treturn subscriptionUsageRevisions.get(usageRevisionKey(provider)) ?? 0;\n}\n\nfunction bumpSubscriptionUsageRevision(provider: string): void {\n\tconst key = usageRevisionKey(provider);\n\tsubscriptionUsageRevisions.set(key, (subscriptionUsageRevisions.get(key) ?? 0) + 1);\n}\n\nexport function supportsSubscriptionUsage(session: UsageSession, provider = session.state.model?.provider): boolean {\n\tconst usageSource = getSubscriptionUsageSource(provider);\n\treturn usageSource?.credentials.some((candidate) => credentialConfigured(session, candidate)) ?? false;\n}\n\n/** Configured quota groups shown in the rail, with the active group first. */\nexport function getConfiguredSubscriptionUsageProviders(session: UsageSession): readonly string[] {\n\tconst activeProvider = session.state.model?.provider;\n\tconst activeSource = getSubscriptionUsageSource(activeProvider);\n\tconst providers = VISIBLE_USAGE_PROVIDERS.filter((provider) => supportsSubscriptionUsage(session, provider));\n\tif (!activeProvider || !activeSource || !supportsSubscriptionUsage(session, activeProvider)) return providers;\n\tconst activeCanonical = providers.find(\n\t\t(provider) => getSubscriptionUsageSource(provider)?.label === activeSource.label,\n\t);\n\tif (!activeCanonical) return [activeProvider, ...providers];\n\treturn [activeCanonical, ...providers.filter((provider) => provider !== activeCanonical)];\n}\n\nexport async function loadSubscriptionUsage(\n\tsession: UsageSession,\n\tfetchImpl: FetchLike = fetch,\n\tprovider = session.state.model?.provider,\n\tqwenCliRunner: QwenCliRunner = runQwenCli,\n): Promise<SubscriptionUsageSnapshot | undefined> {\n\tconst model = session.state.model;\n\tconst usageSource = getSubscriptionUsageSource(provider);\n\tif (!model || !usageSource || !supportsSubscriptionUsage(session, provider)) return undefined;\n\tif (usageSource.kind === \"unavailable\") {\n\t\treturn {\n\t\t\tlabel: usageSource.label,\n\t\t\twindows: [],\n\t\t\tmessage: usageSource.unavailableMessage ?? \"quota API unavailable\",\n\t\t};\n\t}\n\tif (offline()) return { label: usageSource.label, windows: [], message: \"offline\" };\n\tif (usageSource.kind === \"qwen-token-plan\") {\n\t\t// The QwenCloud CLI authenticates itself; no OMK-held credential is used.\n\t\treturn await fetchQwenTokenPlanUsage(usageSource.label, qwenCliRunner);\n\t}\n\n\tconst credential = await resolveCredential(session, usageSource);\n\tif (!credential) return { label: usageSource.label, windows: [], message: \"usage unavailable\" };\n\n\ttry {\n\t\tswitch (usageSource.kind) {\n\t\t\tcase \"codex\":\n\t\t\t\treturn await fetchCodexUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"claude\":\n\t\t\t\treturn await fetchClaudeUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"kimi\":\n\t\t\t\treturn await fetchKimiUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tcase \"zai\":\n\t\t\t\treturn await fetchZaiUsage(usageSource.label, provider ?? model.provider, credential, fetchImpl);\n\t\t\tcase \"grok\":\n\t\t\t\treturn await fetchGrokUsage(usageSource.label, credential.apiKey, fetchImpl);\n\t\t\tdefault:\n\t\t\t\treturn { label: usageSource.label, windows: [], message: \"usage unavailable\" };\n\t\t}\n\t} catch {\n\t\treturn { label: usageSource.label, windows: [], message: \"usage unavailable\" };\n\t}\n}\n\nexport function parseCodexUsageSnapshot(\n\tvalue: unknown,\n\tnowSeconds = Date.now() / 1000,\n): CodexUsageSnapshot | undefined {\n\tconst rateLimit = record(record(value)?.rate_limit);\n\tconst primary = codexWindow(rateLimit?.primary_window, nowSeconds);\n\tconst secondary = codexWindow(rateLimit?.secondary_window, nowSeconds);\n\tconst windows = [primary, secondary].filter((window): window is ParsedCodexWindow => window !== undefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tconst fallbackFiveHour = fiveHour ?? (primary?.windowSeconds === undefined ? primary : undefined);\n\tconst fallbackSevenDay = sevenDay ?? (secondary?.windowSeconds === undefined ? secondary : undefined);\n\tif (!fallbackFiveHour && !fallbackSevenDay) return undefined;\n\treturn {\n\t\t...(fallbackFiveHour ? { fiveHour: publicCodexWindow(fallbackFiveHour) } : {}),\n\t\t...(fallbackSevenDay ? { sevenDay: publicCodexWindow(fallbackSevenDay) } : {}),\n\t};\n}\n\nexport function parseClaudeUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tif (!payload) return undefined;\n\tconst entries = Array.isArray(payload.limits)\n\t\t? payload.limits.slice(0, MAX_USAGE_LIMITS).map(record).filter(isDefined)\n\t\t: [];\n\tconst fiveHour = claudeWindow(payload.five_hour) ?? claudeWindow(entries.find((entry) => entry.kind === \"session\"));\n\tconst sevenDay =\n\t\tclaudeWindow(payload.seven_day) ?? claudeWindow(entries.find((entry) => entry.kind === \"weekly_all\"));\n\tconst windows = [\n\t\tfiveHour ? withLabel(\"5H\", fiveHour) : undefined,\n\t\tsevenDay ? withLabel(\"7D\", sevenDay) : undefined,\n\t].filter(isDefined);\n\treturn windows.length > 0 ? windows : undefined;\n}\n\nexport function parseKimiUsageSnapshot(\n\tvalue: unknown,\n\tnowSeconds = Date.now() / 1000,\n): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tif (!payload) return undefined;\n\tconst windows: SubscriptionUsageWindow[] = [];\n\tconst total = usageRatio(record(payload.usage), nowSeconds);\n\tif (total) windows.push(withLabel(\"TOTAL\", total));\n\tif (Array.isArray(payload.limits)) {\n\t\tfor (const rawLimit of payload.limits.slice(0, MAX_USAGE_LIMITS)) {\n\t\t\tconst limit = record(rawLimit);\n\t\t\tif (!limit) continue;\n\t\t\tconst detail = record(limit.detail) ?? limit;\n\t\t\tconst windowData = record(limit.window);\n\t\t\tconst parsed = usageRatio(detail, nowSeconds);\n\t\t\tif (!parsed) continue;\n\t\t\tconst resetsAt = parseReset(windowData, nowSeconds) ?? parsed.resetsAt;\n\t\t\twindows.push({\n\t\t\t\tlabel: durationLabel(windowData) ?? shortLabel(limit.name ?? limit.title ?? limit.scope, windows.length),\n\t\t\t\tusedPercent: parsed.usedPercent,\n\t\t\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t\t\t});\n\t\t}\n\t}\n\treturn windows.length > 0 ? windows.slice(0, 4) : undefined;\n}\n\n/** Result of one `qwencloud` invocation; `missing` means the CLI is not installed. */\nexport type QwenCliResult =\n\t| { readonly kind: \"missing\" }\n\t| { readonly kind: \"ran\"; readonly exitCode: number; readonly stdout: string };\nexport type QwenCliRunner = (args: readonly string[], timeoutMs: number) => Promise<QwenCliResult>;\n\nexport function buildQwenCliEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n\tconst childEnvironment: NodeJS.ProcessEnv = {};\n\tfor (const key of QWEN_CLI_ENV_KEYS) {\n\t\tconst value = source[key];\n\t\tif (value !== undefined) childEnvironment[key] = value;\n\t}\n\treturn childEnvironment;\n}\n\n/** Parse `qwencloud usage summary --format json` → the token-plan 7-day window. */\nexport function parseQwenTokenPlanUsage(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst tokenPlan = record(record(value)?.token_plan);\n\tif (!tokenPlan || tokenPlan.subscribed === false) return undefined;\n\tconst resetsAt = epochSeconds(tokenPlan.resetDate);\n\tlet usedPercent = finiteNumber(tokenPlan.usedPct) ?? finiteNumber(tokenPlan.used_pct);\n\tif (usedPercent === undefined) {\n\t\tconst total = finiteNumber(tokenPlan.totalCredits);\n\t\tconst remaining = finiteNumber(tokenPlan.remainingCredits);\n\t\tif (total !== undefined && remaining !== undefined && total > 0) {\n\t\t\tusedPercent = ((total - remaining) / total) * 100;\n\t\t}\n\t}\n\tif (usedPercent === undefined) return undefined;\n\treturn [{ label: \"7D\", usedPercent: clampPercent(usedPercent), ...(resetsAt === undefined ? {} : { resetsAt }) }];\n}\n\nexport async function fetchQwenTokenPlanUsage(\n\tlabel: string,\n\trunner: QwenCliRunner = runQwenCli,\n): Promise<SubscriptionUsageSnapshot> {\n\tconst unavailable = (message: string): SubscriptionUsageSnapshot => ({ label, windows: [], message });\n\ttry {\n\t\tconst result = await runner(QWEN_CLI_ARGS, QWEN_CLI_TIMEOUT_MS);\n\t\tif (result.kind === \"missing\") return unavailable(QWEN_CONNECT_HINT);\n\t\tif (result.exitCode === 2) return unavailable(\"run: qwencloud auth login\");\n\t\tif (result.exitCode !== 0 || result.stdout.length > MAX_USAGE_RESPONSE_BYTES) {\n\t\t\treturn unavailable(\"usage unavailable\");\n\t\t}\n\t\tconst payload: unknown = JSON.parse(result.stdout);\n\t\tconst tokenPlan = record(record(payload)?.token_plan);\n\t\tif (tokenPlan?.subscribed === false) return unavailable(\"no active token plan\");\n\t\tconst windows = parseQwenTokenPlanUsage(payload);\n\t\treturn windows ? { label, windows } : unavailable(\"usage unavailable\");\n\t} catch {\n\t\treturn unavailable(\"usage unavailable\");\n\t}\n}\n\nfunction runQwenCli(args: readonly string[], timeoutMs: number): Promise<QwenCliResult> {\n\treturn new Promise((resolve) => {\n\t\tconst binary = process.env.QWENCLOUD_CLI ?? \"qwencloud\";\n\t\texecFile(\n\t\t\tbinary,\n\t\t\t[...args],\n\t\t\t{\n\t\t\t\ttimeout: timeoutMs,\n\t\t\t\tmaxBuffer: MAX_USAGE_RESPONSE_BYTES,\n\t\t\t\twindowsHide: true,\n\t\t\t\tenv: buildQwenCliEnvironment(process.env),\n\t\t\t},\n\t\t\t(error, stdout) => {\n\t\t\t\tif (!error) {\n\t\t\t\t\tresolve({ kind: \"ran\", exitCode: 0, stdout });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst errno = (error as NodeJS.ErrnoException).code;\n\t\t\t\tif (errno === \"ENOENT\") {\n\t\t\t\t\tresolve({ kind: \"missing\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve({ kind: \"ran\", exitCode: typeof errno === \"number\" ? errno : 1, stdout: stdout ?? \"\" });\n\t\t\t},\n\t\t);\n\t});\n}\n\nexport function parseGrokUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst config = record(record(value)?.config);\n\tif (!config) return undefined;\n\tconst period = record(config.currentPeriod);\n\tconst resetsAt = epochSeconds(period?.end) ?? epochSeconds(config.billingPeriodEnd);\n\tconst percent = finiteNumber(config.creditUsagePercent);\n\tconst onDemandUsed = finiteNumber(record(config.onDemandUsed)?.val);\n\tconst onDemandCap = finiteNumber(record(config.onDemandCap)?.val);\n\tlet usedPercent = percent;\n\tif (usedPercent === undefined && onDemandUsed !== undefined && onDemandCap !== undefined && onDemandCap > 0) {\n\t\tusedPercent = (onDemandUsed / onDemandCap) * 100;\n\t}\n\tif (usedPercent === undefined && (period || resetsAt !== undefined)) usedPercent = 0;\n\tif (usedPercent === undefined) return undefined;\n\tconst periodType = typeof period?.type === \"string\" ? period.type : \"\";\n\tconst label = periodType.includes(\"MONTHLY\") ? \"30D\" : \"7D\";\n\treturn [{ label, usedPercent: clampPercent(usedPercent), ...(resetsAt === undefined ? {} : { resetsAt }) }];\n}\n\nexport function parseZaiUsageSnapshot(value: unknown): readonly SubscriptionUsageWindow[] | undefined {\n\tconst payload = record(value);\n\tconst limits = record(payload?.data)?.limits;\n\tif (payload?.success !== true || !Array.isArray(limits)) return undefined;\n\tconst parsed = limits.slice(0, MAX_USAGE_LIMITS).map(zaiWindow).filter(isDefined);\n\tconst requestWindows = parsed.filter((window) => window.type === \"TIME_LIMIT\" && !window.featureOnly);\n\tconst selected =\n\t\trequestWindows.length > 0 ? requestWindows : parsed.filter((window) => window.type === \"TOKENS_LIMIT\");\n\tselected.sort((left, right) => left.durationSeconds - right.durationSeconds);\n\tconst windows = selected\n\t\t.slice(0, 4)\n\t\t.map(({ type: _type, durationSeconds: _duration, featureOnly: _feature, ...window }) => window);\n\treturn windows.length > 0 ? windows : undefined;\n}\n\nexport function recordCodexPassiveUsage(apiKey: string, snapshot: ProviderRateLimitSnapshot, nowMs = Date.now()): void {\n\tconst accountId = codexAccountId(apiKey);\n\tconst limitId = snapshot.limitId?.trim().toLowerCase().replace(/-/g, \"_\");\n\tif (!accountId || !Number.isFinite(nowMs) || nowMs < 0 || (limitId && limitId !== \"codex\")) return;\n\tprunePassiveCodexUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(accountId);\n\tconst primary = normalizePassiveCodexWindow(snapshot.primary, nowMs);\n\tconst secondary = normalizePassiveCodexWindow(snapshot.secondary, nowMs);\n\tif (!primary && !secondary) return;\n\n\tconst previous = passiveCodexUsage.get(cacheKey);\n\tconst next: PassiveUsageEntry = {\n\t\tprimary: primary ? { window: primary, observedAt: nowMs } : freshObservedCodexWindow(previous?.primary, nowMs),\n\t\tsecondary: secondary\n\t\t\t? { window: secondary, observedAt: nowMs }\n\t\t\t: freshObservedCodexWindow(previous?.secondary, nowMs),\n\t};\n\tpassiveCodexUsage.delete(cacheKey);\n\tpassiveCodexUsage.set(cacheKey, next);\n\twhile (passiveCodexUsage.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = passiveCodexUsage.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tpassiveCodexUsage.delete(oldestCacheKey);\n\t}\n\tbumpSubscriptionUsageRevision(\"openai-codex\");\n}\n\nexport function recordClaudePassiveUsage(\n\tapiKey: string,\n\tsnapshot: ProviderRateLimitSnapshot,\n\tnowMs = Date.now(),\n): void {\n\tconst limitId = snapshot.limitId?.trim().toLowerCase().replace(/-/g, \"_\");\n\tif (\n\t\t!apiKey ||\n\t\tapiKey.length > 32_768 ||\n\t\t!Number.isFinite(nowMs) ||\n\t\tnowMs < 0 ||\n\t\t(limitId && limitId !== \"anthropic_unified\")\n\t) {\n\t\treturn;\n\t}\n\tprunePassiveClaudeUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst primary = normalizePassiveCodexWindow(snapshot.primary, nowMs);\n\tconst secondary = normalizePassiveCodexWindow(snapshot.secondary, nowMs);\n\tif (!primary && !secondary) return;\n\n\tconst previous = passiveClaudeUsage.get(cacheKey);\n\tconst next: PassiveUsageEntry = {\n\t\tprimary: primary ? { window: primary, observedAt: nowMs } : freshObservedCodexWindow(previous?.primary, nowMs),\n\t\tsecondary: secondary\n\t\t\t? { window: secondary, observedAt: nowMs }\n\t\t\t: freshObservedCodexWindow(previous?.secondary, nowMs),\n\t};\n\tpassiveClaudeUsage.delete(cacheKey);\n\tpassiveClaudeUsage.set(cacheKey, next);\n\twhile (passiveClaudeUsage.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = passiveClaudeUsage.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tpassiveClaudeUsage.delete(oldestCacheKey);\n\t}\n\tbumpSubscriptionUsageRevision(\"anthropic\");\n}\n\nasync function fetchCodexUsage(\n\tlabel: string,\n\tapiKey: string,\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tconst accountId = codexAccountId(apiKey);\n\tif (!accountId) return { label, windows: [], message: \"usage unavailable\" };\n\tconst passive = passiveCodexSnapshot(apiKey);\n\tlet polled: CodexUsageSnapshot | undefined;\n\ttry {\n\t\tconst payload = await fetchJson(fetchImpl, CODEX_USAGE_URL, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"chatgpt-account-id\": accountId,\n\t\t\t\t\"User-Agent\": \"omk\",\n\t\t\t},\n\t\t});\n\t\tpolled = parseCodexUsageSnapshot(payload);\n\t} catch (error) {\n\t\tif (!passive) throw error;\n\t}\n\tconst merged: CodexUsageSnapshot = {\n\t\tfiveHour: polled?.fiveHour ?? passive?.fiveHour,\n\t\tsevenDay: polled?.sevenDay ?? passive?.sevenDay,\n\t};\n\tif (!merged.fiveHour && !merged.sevenDay) return { label, windows: [], message: \"usage unavailable\" };\n\treturn {\n\t\tlabel,\n\t\twindows: [\n\t\t\tmerged.fiveHour ? withLabel(\"5H\", merged.fiveHour) : undefined,\n\t\t\tmerged.sevenDay ? withLabel(\"7D\", merged.sevenDay) : undefined,\n\t\t].filter(isDefined),\n\t};\n}\n\nasync function fetchClaudeUsage(\n\tlabel: string,\n\tapiKey: string,\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tlet passive = passiveClaudeSnapshot(apiKey);\n\tconst response = await fetchJsonResult(fetchImpl, CLAUDE_USAGE_URL, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"anthropic-beta\": \"claude-code-20250219,oauth-2025-04-20\",\n\t\t\t\"User-Agent\": \"claude-cli/2.1.177 (external, cli)\",\n\t\t},\n\t});\n\tconst windows = [...(parseClaudeUsageSnapshot(response.payload) ?? [])];\n\tif (response.status === 429 && (!passive?.fiveHour || !passive?.sevenDay)) {\n\t\tawait probeClaudeQuota(apiKey, fetchImpl);\n\t\tpassive = passiveClaudeSnapshot(apiKey);\n\t}\n\tfor (const passiveWindow of [\n\t\tpassive?.fiveHour ? withLabel(\"5H\", passive.fiveHour) : undefined,\n\t\tpassive?.sevenDay ? withLabel(\"7D\", passive.sevenDay) : undefined,\n\t].filter(isDefined)) {\n\t\tif (!windows.some((window) => window.label === passiveWindow.label)) windows.push(passiveWindow);\n\t}\n\treturn windows.length > 0\n\t\t? { label, windows }\n\t\t: { label, windows: [], message: response.status === 429 ? \"rate limited · retry later\" : \"usage unavailable\" };\n}\n\n/**\n * Claude Code 2.1.177 performs the same `quota`/`max_tokens: 1` request during startup.\n * Keep it account-scoped and hourly so a pinned sidebar cannot turn the fallback into polling.\n */\nasync function probeClaudeQuota(apiKey: string, fetchImpl: FetchLike, nowMs = Date.now()): Promise<void> {\n\tif (!apiKey || apiKey.length > 32_768 || !Number.isFinite(nowMs) || nowMs < 0) return;\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst activeProbe = claudeQuotaProbesInFlight.get(cacheKey);\n\tif (activeProbe) return activeProbe;\n\tconst lastAttempt = claudeQuotaProbeAttempts.get(cacheKey);\n\tif (lastAttempt !== undefined && nowMs - lastAttempt < CLAUDE_QUOTA_PROBE_COOLDOWN_MS) return;\n\tif (claudeQuotaProbesInFlight.size >= MAX_PASSIVE_ACCOUNTS) return;\n\n\tclaudeQuotaProbeAttempts.delete(cacheKey);\n\tclaudeQuotaProbeAttempts.set(cacheKey, nowMs);\n\twhile (claudeQuotaProbeAttempts.size > MAX_PASSIVE_ACCOUNTS) {\n\t\tconst oldestCacheKey = claudeQuotaProbeAttempts.keys().next().value;\n\t\tif (oldestCacheKey === undefined) break;\n\t\tclaudeQuotaProbeAttempts.delete(oldestCacheKey);\n\t}\n\n\tconst probe = (async () => {\n\t\ttry {\n\t\t\tconst response = await fetchImpl(CLAUDE_MESSAGES_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\t\"anthropic-beta\": \"claude-code-20250219,oauth-2025-04-20\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"User-Agent\": \"claude-cli/2.1.177 (external, cli)\",\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: \"claude-haiku-4-5\",\n\t\t\t\t\tmax_tokens: 1,\n\t\t\t\t\tmessages: [{ role: \"user\", content: \"quota\" }],\n\t\t\t\t}),\n\t\t\t\tsignal: AbortSignal.timeout(CLAUDE_QUOTA_PROBE_TIMEOUT_MS),\n\t\t\t});\n\t\t\tconst snapshot = parseClaudeQuotaProbeHeaders(response.headers, nowMs);\n\t\t\tif (snapshot) recordClaudePassiveUsage(apiKey, snapshot, nowMs);\n\t\t\ttry {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t} catch {\n\t\t\t\t// The quota snapshot lives entirely in response headers.\n\t\t\t}\n\t\t} catch {\n\t\t\t// This mirrors Claude Code's best-effort startup quota check.\n\t\t}\n\t})().finally(() => {\n\t\tclaudeQuotaProbesInFlight.delete(cacheKey);\n\t});\n\tclaudeQuotaProbesInFlight.set(cacheKey, probe);\n\treturn probe;\n}\n\nfunction parseClaudeQuotaProbeHeaders(headers: Headers, nowMs: number): ProviderRateLimitSnapshot | undefined {\n\tconst parseWindow = (prefix: \"5h\" | \"7d\", windowSeconds: number): ProviderRateLimitWindow | undefined => {\n\t\tconst rawUtilization = headers.get(`anthropic-ratelimit-unified-${prefix}-utilization`);\n\t\tconst rawReset = headers.get(`anthropic-ratelimit-unified-${prefix}-reset`);\n\t\tif (rawUtilization === null || rawReset === null) return undefined;\n\t\tconst utilization = Number(rawUtilization);\n\t\tconst resetsAt = Number(rawReset);\n\t\tconst maxResetAt = nowMs / 1000 + 10 * 365 * 24 * 60 * 60;\n\t\tif (\n\t\t\t!Number.isFinite(utilization) ||\n\t\t\tutilization < 0 ||\n\t\t\tutilization > 1 ||\n\t\t\t!Number.isSafeInteger(resetsAt) ||\n\t\t\tresetsAt <= 0 ||\n\t\t\tresetsAt > maxResetAt\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn { usedPercent: Math.round(utilization * 10_000) / 100, windowSeconds, resetsAt };\n\t};\n\tconst primary = parseWindow(\"5h\", FIVE_HOUR_SECONDS);\n\tconst secondary = parseWindow(\"7d\", SEVEN_DAY_SECONDS);\n\treturn primary || secondary ? { limitId: \"anthropic-unified\", primary, secondary } : undefined;\n}\n\nasync function fetchGrokUsage(label: string, apiKey: string, fetchImpl: FetchLike): Promise<SubscriptionUsageSnapshot> {\n\tconst payload = await fetchJson(fetchImpl, GROK_BILLING_URL, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"x-xai-token-auth\": \"xai-grok-cli\",\n\t\t\t\"User-Agent\": \"omk\",\n\t\t},\n\t});\n\tconst windows = parseGrokUsageSnapshot(payload);\n\treturn windows ? { label, windows } : { label, windows: [], message: \"usage unavailable\" };\n}\n\nasync function fetchKimiUsage(label: string, apiKey: string, fetchImpl: FetchLike): Promise<SubscriptionUsageSnapshot> {\n\tconst payload = await fetchJson(fetchImpl, KIMI_USAGE_URL, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": \"KimiCLI/1.5\",\n\t\t\t\"X-Msh-Platform\": \"kimi_cli\",\n\t\t\t\"X-Msh-Version\": \"1.5\",\n\t\t},\n\t});\n\tconst windows = parseKimiUsageSnapshot(payload);\n\treturn windows ? { label, windows } : { label, windows: [], message: \"usage unavailable\" };\n}\n\nasync function fetchZaiUsage(\n\tlabel: string,\n\tmodelProvider: string,\n\tcredential: { readonly provider: string; readonly apiKey: string },\n\tfetchImpl: FetchLike,\n): Promise<SubscriptionUsageSnapshot> {\n\tconst origin =\n\t\tmodelProvider === \"zai-coding-cn\" ||\n\t\tcredential.provider === \"zai-coding-cn\" ||\n\t\tcredential.provider === \"zhipu-coding-plan\"\n\t\t\t? \"https://open.bigmodel.cn\"\n\t\t\t: \"https://api.z.ai\";\n\tconst payload = await fetchJson(fetchImpl, `${origin}/api/monitor/usage/quota/limit`, {\n\t\theaders: {\n\t\t\tAuthorization: credential.apiKey,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": \"omk\",\n\t\t},\n\t});\n\tconst windows = parseZaiUsageSnapshot(payload);\n\treturn windows ? { label, windows } : { label, windows: [], message: \"usage unavailable\" };\n}\n\nasync function fetchJson(fetchImpl: FetchLike, url: string, init: RequestInit): Promise<unknown> {\n\treturn (await fetchJsonResult(fetchImpl, url, init)).payload;\n}\n\nasync function fetchJsonResult(fetchImpl: FetchLike, url: string, init: RequestInit): Promise<FetchJsonResult> {\n\tconst response = await fetchImpl(url, { ...init, signal: AbortSignal.timeout(10_000) });\n\tif (!response.ok) return { status: response.status };\n\tconst declaredBytes = Number(response.headers?.get(\"content-length\"));\n\tif (Number.isFinite(declaredBytes) && declaredBytes > MAX_USAGE_RESPONSE_BYTES) {\n\t\treturn { status: response.status };\n\t}\n\tif (!response.body || typeof response.body.getReader !== \"function\") {\n\t\treturn { status: response.status, payload: await response.json() };\n\t}\n\n\tconst reader = response.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet totalBytes = 0;\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotalBytes += value.byteLength;\n\t\tif (totalBytes > MAX_USAGE_RESPONSE_BYTES) {\n\t\t\tawait reader.cancel();\n\t\t\treturn { status: response.status };\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst body = new Uint8Array(totalBytes);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbody.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\ttry {\n\t\treturn { status: response.status, payload: JSON.parse(new TextDecoder().decode(body)) as unknown };\n\t} catch {\n\t\treturn { status: response.status };\n\t}\n}\n\nfunction credentialConfigured(session: UsageSession, candidate: CredentialCandidate): boolean {\n\tif (candidate.oauthOnly) return session.modelRegistry.isUsingOAuthProvider(candidate.provider);\n\tconst status = session.modelRegistry.getProviderAuthStatus(candidate.provider);\n\treturn status.configured || status.source !== undefined;\n}\n\nasync function resolveCredential(\n\tsession: UsageSession,\n\tusageSource: SubscriptionUsageSource,\n): Promise<{ readonly provider: string; readonly apiKey: string } | undefined> {\n\tfor (const candidate of usageSource.credentials) {\n\t\tif (!credentialConfigured(session, candidate)) continue;\n\t\tconst apiKey = await session.modelRegistry.getApiKeyForProvider(candidate.provider);\n\t\tif (apiKey) return { provider: candidate.provider, apiKey };\n\t}\n\treturn undefined;\n}\n\nfunction codexWindow(value: unknown, nowSeconds: number): ParsedCodexWindow | undefined {\n\tconst window = record(value);\n\tconst usedPercent = finiteNumber(window?.used_percent);\n\tif (usedPercent === undefined) return undefined;\n\tconst windowSeconds = finiteNumber(window?.limit_window_seconds);\n\tconst resetAt = finiteNumber(window?.reset_at);\n\tconst resetAfter = finiteNumber(window?.reset_after_seconds);\n\tconst resetsAt = resetAt ?? (resetAfter === undefined ? undefined : nowSeconds + resetAfter);\n\treturn {\n\t\tusedPercent: clampPercent(usedPercent),\n\t\t...(windowSeconds === undefined ? {} : { windowSeconds }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction claudeWindow(value: unknown): Omit<SubscriptionUsageWindow, \"label\"> | undefined {\n\tconst bucket = record(value);\n\tif (!bucket || bucket.is_active === false) return undefined;\n\tconst usedPercent = finiteNumber(bucket.utilization ?? bucket.percent);\n\tif (usedPercent === undefined) return undefined;\n\tconst resetsAt = epochSeconds(bucket.resets_at);\n\treturn { usedPercent: clampPercent(usedPercent), ...(resetsAt === undefined ? {} : { resetsAt }) };\n}\n\nfunction usageRatio(\n\tdata: Record<string, unknown> | undefined,\n\tnowSeconds: number,\n): Omit<SubscriptionUsageWindow, \"label\"> | undefined {\n\tif (!data) return undefined;\n\tconst limit = finiteNumber(data.limit);\n\tlet used = finiteNumber(data.used);\n\tconst remaining = finiteNumber(data.remaining);\n\tif (used === undefined && limit !== undefined && remaining !== undefined) used = limit - remaining;\n\tif (used === undefined || limit === undefined || limit <= 0) return undefined;\n\tconst resetsAt = parseReset(data, nowSeconds);\n\treturn {\n\t\tusedPercent: clampPercent((used / limit) * 100),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction zaiWindow(value: unknown):\n\t| (SubscriptionUsageWindow & {\n\t\t\treadonly type: string;\n\t\t\treadonly durationSeconds: number;\n\t\t\treadonly featureOnly: boolean;\n\t })\n\t| undefined {\n\tconst item = record(value);\n\tif (!item || (item.type !== \"TIME_LIMIT\" && item.type !== \"TOKENS_LIMIT\")) return undefined;\n\tconst percentage = finiteNumber(item.percentage);\n\tconst current = finiteNumber(item.currentValue);\n\tconst limit = finiteNumber(item.usage);\n\tconst usedPercent =\n\t\tpercentage ?? (current !== undefined && limit !== undefined && limit > 0 ? (current / limit) * 100 : undefined);\n\tif (usedPercent === undefined) return undefined;\n\tconst duration = zaiDuration(finiteNumber(item.unit), finiteNumber(item.number));\n\tconst resetsAt = epochSeconds(item.nextResetTime);\n\tconst detailCodes = Array.isArray(item.usageDetails)\n\t\t? item.usageDetails\n\t\t\t\t.map((detail) => record(detail)?.modelCode)\n\t\t\t\t.filter((code): code is string => typeof code === \"string\")\n\t\t: [];\n\treturn {\n\t\ttype: item.type,\n\t\tlabel: duration.label,\n\t\tdurationSeconds: duration.seconds,\n\t\tfeatureOnly: [\"search-prime\", \"web-reader\", \"zread\"].every((code) => detailCodes.includes(code)),\n\t\tusedPercent: clampPercent(usedPercent),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction zaiDuration(unit: number | undefined, countValue: number | undefined): { label: string; seconds: number } {\n\tconst count = countValue && countValue > 0 ? countValue : 1;\n\tif (unit === 3) return { label: `${count}H`, seconds: count * 60 * 60 };\n\tif (unit === 4) return { label: `${count}D`, seconds: count * 24 * 60 * 60 };\n\tif (unit === 5) return { label: count === 1 ? \"30D\" : `${count}MO`, seconds: count * 30 * 24 * 60 * 60 };\n\tif (unit === 6) return { label: \"7D\", seconds: 7 * 24 * 60 * 60 };\n\treturn { label: \"QUOTA\", seconds: Number.POSITIVE_INFINITY };\n}\n\nfunction durationLabel(window: Record<string, unknown> | undefined): string | undefined {\n\tif (!window) return undefined;\n\tconst duration = finiteNumber(window.duration);\n\tconst unit = typeof window.timeUnit === \"string\" ? window.timeUnit.toUpperCase() : \"\";\n\tif (duration === undefined) return undefined;\n\tif (unit.includes(\"MINUTE\")) return duration % 60 === 0 ? `${duration / 60}H` : `${duration}M`;\n\tif (unit.includes(\"HOUR\")) return `${duration}H`;\n\tif (unit.includes(\"DAY\")) return `${duration}D`;\n\tif (unit.includes(\"SECOND\")) return `${duration}S`;\n\treturn undefined;\n}\n\nfunction parseReset(data: Record<string, unknown> | undefined, nowSeconds: number): number | undefined {\n\tif (!data) return undefined;\n\tfor (const key of [\"reset_at\", \"resetAt\", \"reset_time\", \"resetTime\"] as const) {\n\t\tconst parsed = epochSeconds(data[key]);\n\t\tif (parsed !== undefined) return parsed;\n\t}\n\tfor (const key of [\"reset_in\", \"resetIn\", \"ttl\"] as const) {\n\t\tconst seconds = finiteNumber(data[key]);\n\t\tif (seconds !== undefined) return nowSeconds + seconds;\n\t}\n\treturn undefined;\n}\n\nfunction epochSeconds(value: unknown): number | undefined {\n\tconst numeric = finiteNumber(value);\n\tif (numeric !== undefined) return numeric > 1_000_000_000_000 ? numeric / 1000 : numeric;\n\tif (typeof value !== \"string\" || !value.trim()) return undefined;\n\tconst parsed = Date.parse(value);\n\treturn Number.isFinite(parsed) ? parsed / 1000 : undefined;\n}\n\nfunction normalizePassiveCodexWindow(\n\twindow: ProviderRateLimitWindow | undefined,\n\tnowMs: number,\n): ParsedCodexWindow | undefined {\n\tif (!window || !Number.isFinite(window.usedPercent)) return undefined;\n\tconst windowSeconds =\n\t\twindow.windowSeconds !== undefined &&\n\t\tNumber.isFinite(window.windowSeconds) &&\n\t\twindow.windowSeconds > 0 &&\n\t\twindow.windowSeconds <= Number.MAX_SAFE_INTEGER\n\t\t\t? Math.round(window.windowSeconds)\n\t\t\t: undefined;\n\tconst maxResetAt = nowMs / 1000 + 10 * 365 * 24 * 60 * 60;\n\tconst resetsAtValue = window.resetsAt === undefined ? undefined : Math.floor(window.resetsAt);\n\tconst resetsAt =\n\t\tresetsAtValue !== undefined &&\n\t\tNumber.isSafeInteger(resetsAtValue) &&\n\t\tresetsAtValue > 0 &&\n\t\tresetsAtValue <= maxResetAt\n\t\t\t? resetsAtValue\n\t\t\t: undefined;\n\treturn {\n\t\tusedPercent: clampPercent(window.usedPercent),\n\t\t...(windowSeconds === undefined ? {} : { windowSeconds }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction passiveCredentialCacheKey(identifier: string): string {\n\treturn createHash(\"sha256\").update(identifier).digest(\"base64url\");\n}\n\nfunction freshObservedCodexWindow(\n\tobserved: ObservedCodexWindow | undefined,\n\tnowMs: number,\n): ObservedCodexWindow | undefined {\n\tif (!observed || nowMs - observed.observedAt > PASSIVE_USAGE_TTL_MS) return undefined;\n\tif (observed.window.resetsAt !== undefined && observed.window.resetsAt <= nowMs / 1000) return undefined;\n\treturn observed;\n}\n\nfunction prunePassiveCodexUsage(nowMs: number): void {\n\tfor (const [cacheKey, entry] of passiveCodexUsage) {\n\t\tif (!freshObservedCodexWindow(entry.primary, nowMs) && !freshObservedCodexWindow(entry.secondary, nowMs)) {\n\t\t\tpassiveCodexUsage.delete(cacheKey);\n\t\t}\n\t}\n}\n\nfunction prunePassiveClaudeUsage(nowMs: number): void {\n\tfor (const [cacheKey, entry] of passiveClaudeUsage) {\n\t\tif (!freshObservedCodexWindow(entry.primary, nowMs) && !freshObservedCodexWindow(entry.secondary, nowMs)) {\n\t\t\tpassiveClaudeUsage.delete(cacheKey);\n\t\t}\n\t}\n}\n\nfunction passiveCodexSnapshot(apiKey: string, nowMs = Date.now()): CodexUsageSnapshot | undefined {\n\tconst accountId = codexAccountId(apiKey);\n\tif (!accountId || !Number.isFinite(nowMs) || nowMs < 0) return undefined;\n\tprunePassiveCodexUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(accountId);\n\tconst entry = passiveCodexUsage.get(cacheKey);\n\tif (!entry) return undefined;\n\tconst primary = freshObservedCodexWindow(entry.primary, nowMs);\n\tconst secondary = freshObservedCodexWindow(entry.secondary, nowMs);\n\tif (!primary && !secondary) {\n\t\tpassiveCodexUsage.delete(cacheKey);\n\t\treturn undefined;\n\t}\n\tconst freshEntry = { primary, secondary };\n\tpassiveCodexUsage.delete(cacheKey);\n\tpassiveCodexUsage.set(cacheKey, freshEntry);\n\n\tconst windows = [primary?.window, secondary?.window].filter(isDefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tif (!fiveHour && !sevenDay) return undefined;\n\treturn {\n\t\t...(fiveHour ? { fiveHour: publicCodexWindow(fiveHour) } : {}),\n\t\t...(sevenDay ? { sevenDay: publicCodexWindow(sevenDay) } : {}),\n\t};\n}\n\nfunction passiveClaudeSnapshot(apiKey: string, nowMs = Date.now()): CodexUsageSnapshot | undefined {\n\tif (!apiKey || apiKey.length > 32_768 || !Number.isFinite(nowMs) || nowMs < 0) return undefined;\n\tprunePassiveClaudeUsage(nowMs);\n\tconst cacheKey = passiveCredentialCacheKey(apiKey);\n\tconst entry = passiveClaudeUsage.get(cacheKey);\n\tif (!entry) return undefined;\n\tconst primary = freshObservedCodexWindow(entry.primary, nowMs);\n\tconst secondary = freshObservedCodexWindow(entry.secondary, nowMs);\n\tif (!primary && !secondary) {\n\t\tpassiveClaudeUsage.delete(cacheKey);\n\t\treturn undefined;\n\t}\n\tpassiveClaudeUsage.delete(cacheKey);\n\tpassiveClaudeUsage.set(cacheKey, { primary, secondary });\n\n\tconst windows = [primary?.window, secondary?.window].filter(isDefined);\n\tconst fiveHour = windows.find((window) => near(window.windowSeconds, FIVE_HOUR_SECONDS));\n\tconst sevenDay = windows.find((window) => near(window.windowSeconds, SEVEN_DAY_SECONDS));\n\tif (!fiveHour && !sevenDay) return undefined;\n\treturn {\n\t\t...(fiveHour ? { fiveHour: publicCodexWindow(fiveHour) } : {}),\n\t\t...(sevenDay ? { sevenDay: publicCodexWindow(sevenDay) } : {}),\n\t};\n}\n\nfunction codexAccountId(token: string): string | undefined {\n\ttry {\n\t\tconst payload = token.split(\".\")[1];\n\t\tif (!payload) return undefined;\n\t\tconst claims = record(JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")));\n\t\tconst auth = record(claims?.[OPENAI_AUTH_CLAIM]);\n\t\tconst accountId = typeof auth?.chatgpt_account_id === \"string\" ? auth.chatgpt_account_id.trim() : \"\";\n\t\treturn accountId && accountId.length <= 256 ? accountId : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction publicCodexWindow(window: ParsedCodexWindow): CodexUsageWindow {\n\treturn { usedPercent: window.usedPercent, ...(window.resetsAt === undefined ? {} : { resetsAt: window.resetsAt }) };\n}\n\nfunction withLabel(label: string, window: Omit<SubscriptionUsageWindow, \"label\">): SubscriptionUsageWindow {\n\treturn { label, ...window };\n}\n\nfunction shortLabel(value: unknown, index: number): string {\n\tif (typeof value !== \"string\") return `LIMIT${index + 1}`;\n\tconst safe = value\n\t\t.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u206f]/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\treturn safe ? safe.toUpperCase().slice(0, 8) : `LIMIT${index + 1}`;\n}\n\nfunction finiteNumber(value: unknown): number | undefined {\n\tconst number =\n\t\ttypeof value === \"number\" ? value : typeof value === \"string\" && value.trim() ? Number(value) : Number.NaN;\n\treturn Number.isFinite(number) ? number : undefined;\n}\n\nfunction clampPercent(value: number): number {\n\treturn Number(Math.max(0, Math.min(100, value)).toFixed(2));\n}\n\nfunction near(actual: number | undefined, expected: number): boolean {\n\treturn actual !== undefined && Math.abs(actual - expected) <= WINDOW_TOLERANCE_SECONDS;\n}\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: undefined;\n}\n\nfunction isDefined<T>(value: T | undefined): value is T {\n\treturn value !== undefined;\n}\n\nfunction offline(): boolean {\n\tconst value = process.env.OMK_OFFLINE?.toLowerCase();\n\treturn value === \"1\" || value === \"true\" || value === \"yes\";\n}\n"]}