@ai-devkit/agent-manager 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/dist/AgentManager.d.ts +0 -4
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +2 -2
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/capacity/anthropic.test.js +115 -0
  6. package/dist/__tests__/capacity/anthropic.test.js.map +1 -0
  7. package/dist/__tests__/capacity/boundaries.test.js +45 -0
  8. package/dist/__tests__/capacity/boundaries.test.js.map +1 -0
  9. package/dist/__tests__/capacity/claude.test.js +3 -3
  10. package/dist/__tests__/capacity/claude.test.js.map +1 -1
  11. package/dist/__tests__/capacity/codex.test.js +3 -1
  12. package/dist/__tests__/capacity/codex.test.js.map +1 -1
  13. package/dist/__tests__/capacity/openai.test.js +21 -12
  14. package/dist/__tests__/capacity/openai.test.js.map +1 -1
  15. package/dist/__tests__/capacity/wham.test.js +1 -1
  16. package/dist/__tests__/capacity/wham.test.js.map +1 -1
  17. package/dist/__tests__/capacity/zai.test.js +7 -8
  18. package/dist/__tests__/capacity/zai.test.js.map +1 -1
  19. package/dist/__tests__/harnesses/claude/durable/ClaudePrintAgent.integration.test.js +2 -1
  20. package/dist/__tests__/harnesses/claude/durable/ClaudePrintAgent.integration.test.js.map +1 -1
  21. package/dist/__tests__/harnesses/claude/durable/ClaudePrintRunner.test.js.map +1 -1
  22. package/dist/__tests__/harnesses/codex/durable/CodexCliProbe.test.js +7 -15
  23. package/dist/__tests__/harnesses/codex/durable/CodexCliProbe.test.js.map +1 -1
  24. package/dist/__tests__/harnesses/codex/durable/CodexPrintAgent.integration.test.js +3 -1
  25. package/dist/__tests__/harnesses/codex/durable/CodexPrintAgent.integration.test.js.map +1 -1
  26. package/dist/__tests__/harnesses/codex/durable/CodexPrintAgentService.test.js +11 -26
  27. package/dist/__tests__/harnesses/codex/durable/CodexPrintAgentService.test.js.map +1 -1
  28. package/dist/__tests__/harnesses/codex/durable/CodexPrintRunner.test.js +3 -7
  29. package/dist/__tests__/harnesses/codex/durable/CodexPrintRunner.test.js.map +1 -1
  30. package/dist/__tests__/harnesses/pi/durable/PiCliProbe.test.js +5 -11
  31. package/dist/__tests__/harnesses/pi/durable/PiCliProbe.test.js.map +1 -1
  32. package/dist/__tests__/harnesses/pi/durable/PiPrintAgentService.test.js +9 -19
  33. package/dist/__tests__/harnesses/pi/durable/PiPrintAgentService.test.js.map +1 -1
  34. package/dist/__tests__/harnesses/pi/durable/PiPrintRunner.test.js +3 -7
  35. package/dist/__tests__/harnesses/pi/durable/PiPrintRunner.test.js.map +1 -1
  36. package/dist/__tests__/{readiness → harnesses/readiness}/AgentReadiness.test.js +31 -10
  37. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -0
  38. package/dist/__tests__/harnesses/runtimeProfiles.test.js +89 -0
  39. package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -0
  40. package/dist/__tests__/print/DurableAgent.test.js +4 -6
  41. package/dist/__tests__/print/DurableAgent.test.js.map +1 -1
  42. package/dist/__tests__/print/DurableAgentRepository.test.js +1 -12
  43. package/dist/__tests__/print/DurableAgentRepository.test.js.map +1 -1
  44. package/dist/__tests__/runtime/ManagedAgentRuntime.test.js.map +1 -1
  45. package/dist/__tests__/terminal/TmuxManager.test.js +1 -1
  46. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -1
  47. package/dist/capacity/index.d.ts +9 -19
  48. package/dist/capacity/index.d.ts.map +1 -1
  49. package/dist/capacity/index.js +11 -4
  50. package/dist/capacity/index.js.map +1 -1
  51. package/dist/capacity/providers/anthropic.d.ts +9 -0
  52. package/dist/capacity/providers/anthropic.d.ts.map +1 -0
  53. package/dist/capacity/{claude.js → providers/anthropic.js} +10 -128
  54. package/dist/capacity/providers/anthropic.js.map +1 -0
  55. package/dist/capacity/providers/openai-platform.d.ts +14 -0
  56. package/dist/capacity/providers/openai-platform.d.ts.map +1 -0
  57. package/dist/capacity/providers/openai-platform.js +120 -0
  58. package/dist/capacity/providers/openai-platform.js.map +1 -0
  59. package/dist/capacity/providers/openai-subscription.d.ts +20 -0
  60. package/dist/capacity/providers/openai-subscription.d.ts.map +1 -0
  61. package/dist/capacity/providers/openai-subscription.js +129 -0
  62. package/dist/capacity/providers/openai-subscription.js.map +1 -0
  63. package/dist/capacity/providers/zai.d.ts +8 -0
  64. package/dist/capacity/providers/zai.d.ts.map +1 -0
  65. package/dist/capacity/{zai.js → providers/zai.js} +17 -62
  66. package/dist/capacity/providers/zai.js.map +1 -0
  67. package/dist/capacity/sources/claude.d.ts +9 -0
  68. package/dist/capacity/sources/claude.d.ts.map +1 -0
  69. package/dist/capacity/sources/claude.js +17 -0
  70. package/dist/capacity/sources/claude.js.map +1 -0
  71. package/dist/capacity/{codex.d.ts → sources/codex.d.ts} +5 -9
  72. package/dist/capacity/sources/codex.d.ts.map +1 -0
  73. package/dist/capacity/{codex.js → sources/codex.js} +31 -84
  74. package/dist/capacity/sources/codex.js.map +1 -0
  75. package/dist/capacity/sources/pi.d.ts +30 -0
  76. package/dist/capacity/sources/pi.d.ts.map +1 -0
  77. package/dist/capacity/sources/pi.js +81 -0
  78. package/dist/capacity/sources/pi.js.map +1 -0
  79. package/dist/capacity/types.d.ts +2 -0
  80. package/dist/capacity/types.d.ts.map +1 -1
  81. package/dist/capacity/types.js +1 -1
  82. package/dist/capacity/types.js.map +1 -1
  83. package/dist/harnesses/claude/credentials.d.ts +10 -0
  84. package/dist/harnesses/claude/credentials.d.ts.map +1 -0
  85. package/dist/harnesses/claude/credentials.js +112 -0
  86. package/dist/harnesses/claude/credentials.js.map +1 -0
  87. package/dist/harnesses/claude/readiness.d.ts +3 -0
  88. package/dist/harnesses/claude/readiness.d.ts.map +1 -0
  89. package/dist/harnesses/claude/readiness.js +63 -0
  90. package/dist/harnesses/claude/readiness.js.map +1 -0
  91. package/dist/harnesses/codex/credentials.d.ts +15 -0
  92. package/dist/harnesses/codex/credentials.d.ts.map +1 -0
  93. package/dist/harnesses/codex/credentials.js +58 -0
  94. package/dist/harnesses/codex/credentials.js.map +1 -0
  95. package/dist/harnesses/codex/readiness.d.ts +3 -0
  96. package/dist/harnesses/codex/readiness.d.ts.map +1 -0
  97. package/dist/harnesses/codex/readiness.js +60 -0
  98. package/dist/harnesses/codex/readiness.js.map +1 -0
  99. package/dist/harnesses/copilot/readiness.d.ts +3 -0
  100. package/dist/harnesses/copilot/readiness.d.ts.map +1 -0
  101. package/dist/harnesses/copilot/readiness.js +39 -0
  102. package/dist/harnesses/copilot/readiness.js.map +1 -0
  103. package/dist/harnesses/opencode/readiness.d.ts +3 -0
  104. package/dist/harnesses/opencode/readiness.d.ts.map +1 -0
  105. package/dist/harnesses/opencode/readiness.js +50 -0
  106. package/dist/harnesses/opencode/readiness.js.map +1 -0
  107. package/dist/harnesses/pi/credentials.d.ts +22 -0
  108. package/dist/harnesses/pi/credentials.d.ts.map +1 -0
  109. package/dist/harnesses/pi/credentials.js +98 -0
  110. package/dist/harnesses/pi/credentials.js.map +1 -0
  111. package/dist/harnesses/pi/readiness.d.ts +3 -0
  112. package/dist/harnesses/pi/readiness.d.ts.map +1 -0
  113. package/dist/harnesses/pi/readiness.js +96 -0
  114. package/dist/harnesses/pi/readiness.js.map +1 -0
  115. package/dist/harnesses/readiness/AgentReadiness.d.ts +6 -0
  116. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -0
  117. package/dist/harnesses/readiness/AgentReadiness.js +68 -0
  118. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -0
  119. package/dist/harnesses/readiness/checks.d.ts +31 -0
  120. package/dist/harnesses/readiness/checks.d.ts.map +1 -0
  121. package/dist/harnesses/readiness/checks.js +270 -0
  122. package/dist/harnesses/readiness/checks.js.map +1 -0
  123. package/dist/{readiness/AgentReadiness.d.ts → harnesses/readiness/types.d.ts} +19 -10
  124. package/dist/harnesses/readiness/types.d.ts.map +1 -0
  125. package/dist/harnesses/readiness/types.js +4 -0
  126. package/dist/harnesses/readiness/types.js.map +1 -0
  127. package/dist/harnesses/runtimeProfiles.d.ts +11 -0
  128. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -0
  129. package/dist/harnesses/runtimeProfiles.js +99 -0
  130. package/dist/harnesses/runtimeProfiles.js.map +1 -0
  131. package/dist/index.d.ts +16 -42
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +9 -23
  134. package/dist/index.js.map +1 -1
  135. package/dist/runtime/ManagedAgentRuntime.d.ts +3 -3
  136. package/dist/runtime/ManagedAgentRuntime.d.ts.map +1 -1
  137. package/dist/runtime/ManagedAgentRuntime.js +6 -20
  138. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  139. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -1
  140. package/dist/runtime/herdr/HerdrAgentDiscovery.js +2 -43
  141. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  142. package/dist/runtime/tmux/PidPolling.d.ts +1 -1
  143. package/dist/runtime/tmux/PidPolling.d.ts.map +1 -1
  144. package/dist/runtime/tmux/PidPolling.js.map +1 -1
  145. package/dist/runtime/tmux/TmuxAgentRuntime.d.ts +1 -1
  146. package/dist/runtime/tmux/TmuxAgentRuntime.d.ts.map +1 -1
  147. package/dist/runtime/tmux/TmuxAgentRuntime.js +1 -1
  148. package/dist/runtime/tmux/TmuxAgentRuntime.js.map +1 -1
  149. package/dist/runtime/tmux/TmuxManager.d.ts +12 -0
  150. package/dist/runtime/tmux/TmuxManager.d.ts.map +1 -0
  151. package/dist/{terminal → runtime/tmux}/TmuxManager.js +9 -28
  152. package/dist/runtime/tmux/TmuxManager.js.map +1 -0
  153. package/dist/runtime/types.d.ts +1 -1
  154. package/dist/runtime/types.d.ts.map +1 -1
  155. package/dist/runtime/types.js.map +1 -1
  156. package/dist/terminal/TerminalFocusManager.d.ts +4 -32
  157. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  158. package/dist/terminal/TerminalFocusManager.js +17 -267
  159. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  160. package/dist/terminal/TtyWriter.d.ts +1 -44
  161. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  162. package/dist/terminal/TtyWriter.js +11 -347
  163. package/dist/terminal/TtyWriter.js.map +1 -1
  164. package/dist/terminal/backends/index.d.ts +5 -0
  165. package/dist/terminal/backends/index.d.ts.map +1 -0
  166. package/dist/terminal/backends/index.js +19 -0
  167. package/dist/terminal/backends/index.js.map +1 -0
  168. package/dist/terminal/backends/iterm2.d.ts +3 -0
  169. package/dist/terminal/backends/iterm2.d.ts.map +1 -0
  170. package/dist/terminal/backends/iterm2.js +135 -0
  171. package/dist/terminal/backends/iterm2.js.map +1 -0
  172. package/dist/terminal/backends/shared.d.ts +7 -0
  173. package/dist/terminal/backends/shared.d.ts.map +1 -0
  174. package/dist/terminal/backends/shared.js +35 -0
  175. package/dist/terminal/backends/shared.js.map +1 -0
  176. package/dist/terminal/backends/terminal-app.d.ts +3 -0
  177. package/dist/terminal/backends/terminal-app.d.ts.map +1 -0
  178. package/dist/terminal/backends/terminal-app.js +129 -0
  179. package/dist/terminal/backends/terminal-app.js.map +1 -0
  180. package/dist/terminal/backends/tmux.d.ts +3 -0
  181. package/dist/terminal/backends/tmux.d.ts.map +1 -0
  182. package/dist/terminal/backends/tmux.js +76 -0
  183. package/dist/terminal/backends/tmux.js.map +1 -0
  184. package/dist/terminal/backends/wezterm.d.ts +3 -0
  185. package/dist/terminal/backends/wezterm.d.ts.map +1 -0
  186. package/dist/terminal/backends/wezterm.js +71 -0
  187. package/dist/terminal/backends/wezterm.js.map +1 -0
  188. package/dist/terminal/types.d.ts +21 -0
  189. package/dist/terminal/types.d.ts.map +1 -0
  190. package/dist/terminal/types.js +10 -0
  191. package/dist/terminal/types.js.map +1 -0
  192. package/package.json +1 -1
  193. package/src/AgentManager.ts +1 -1
  194. package/src/__tests__/capacity/anthropic.test.ts +105 -0
  195. package/src/__tests__/capacity/boundaries.test.ts +36 -0
  196. package/src/__tests__/capacity/claude.test.ts +3 -8
  197. package/src/__tests__/capacity/codex.test.ts +12 -35
  198. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +1 -1
  199. package/src/__tests__/capacity/openai.test.ts +15 -15
  200. package/src/__tests__/capacity/wham.test.ts +1 -1
  201. package/src/__tests__/capacity/zai.test.ts +27 -62
  202. package/src/__tests__/harnesses/claude/durable/ClaudePrintAgent.integration.test.ts +2 -2
  203. package/src/__tests__/harnesses/claude/durable/ClaudePrintRunner.test.ts +1 -1
  204. package/src/__tests__/harnesses/codex/durable/CodexCliProbe.test.ts +14 -19
  205. package/src/__tests__/harnesses/codex/durable/CodexPrintAgent.integration.test.ts +3 -3
  206. package/src/__tests__/harnesses/codex/durable/CodexPrintAgentService.test.ts +24 -30
  207. package/src/__tests__/harnesses/codex/durable/CodexPrintRunner.test.ts +4 -8
  208. package/src/__tests__/harnesses/pi/durable/PiCliProbe.test.ts +7 -11
  209. package/src/__tests__/harnesses/pi/durable/PiPrintAgentService.test.ts +19 -22
  210. package/src/__tests__/harnesses/pi/durable/PiPrintRunner.test.ts +7 -8
  211. package/src/__tests__/{readiness → harnesses/readiness}/AgentReadiness.test.ts +36 -9
  212. package/src/__tests__/harnesses/runtimeProfiles.test.ts +110 -0
  213. package/src/__tests__/print/DurableAgent.test.ts +4 -13
  214. package/src/__tests__/print/DurableAgentRepository.test.ts +1 -13
  215. package/src/__tests__/runtime/ManagedAgentRuntime.test.ts +1 -1
  216. package/src/__tests__/terminal/TmuxManager.test.ts +1 -1
  217. package/src/capacity/index.ts +29 -23
  218. package/src/capacity/{claude.ts → providers/anthropic.ts} +16 -131
  219. package/src/capacity/providers/openai-platform.ts +150 -0
  220. package/src/capacity/{wham.ts → providers/openai-subscription.ts} +86 -8
  221. package/src/capacity/{zai.ts → providers/zai.ts} +29 -111
  222. package/src/capacity/sources/claude.ts +30 -0
  223. package/src/capacity/{codex.ts → sources/codex.ts} +54 -124
  224. package/src/capacity/sources/pi.ts +131 -0
  225. package/src/capacity/types.ts +3 -0
  226. package/src/harnesses/claude/credentials.ts +122 -0
  227. package/src/harnesses/claude/readiness.ts +69 -0
  228. package/src/harnesses/codex/credentials.ts +81 -0
  229. package/src/harnesses/codex/readiness.ts +72 -0
  230. package/src/harnesses/copilot/readiness.ts +33 -0
  231. package/src/harnesses/opencode/readiness.ts +48 -0
  232. package/src/harnesses/pi/credentials.ts +129 -0
  233. package/src/harnesses/pi/readiness.ts +104 -0
  234. package/src/harnesses/readiness/AgentReadiness.ts +87 -0
  235. package/src/harnesses/readiness/checks.ts +336 -0
  236. package/src/harnesses/readiness/types.ts +90 -0
  237. package/src/{utils/agents.ts → harnesses/runtimeProfiles.ts} +34 -38
  238. package/src/index.ts +15 -128
  239. package/src/runtime/ManagedAgentRuntime.ts +7 -22
  240. package/src/runtime/herdr/HerdrAgentDiscovery.ts +3 -19
  241. package/src/runtime/tmux/PidPolling.ts +1 -1
  242. package/src/runtime/tmux/TmuxAgentRuntime.ts +1 -1
  243. package/src/{terminal → runtime/tmux}/TmuxManager.ts +9 -30
  244. package/src/runtime/types.ts +1 -1
  245. package/src/terminal/TerminalFocusManager.ts +19 -292
  246. package/src/terminal/TtyWriter.ts +16 -336
  247. package/src/terminal/backends/index.ts +21 -0
  248. package/src/terminal/backends/iterm2.ts +126 -0
  249. package/src/terminal/backends/shared.ts +42 -0
  250. package/src/terminal/backends/terminal-app.ts +122 -0
  251. package/src/terminal/backends/tmux.ts +62 -0
  252. package/src/terminal/backends/wezterm.ts +65 -0
  253. package/src/terminal/types.ts +22 -0
  254. package/dist/__tests__/readiness/AgentReadiness.test.js.map +0 -1
  255. package/dist/__tests__/utils/agents.test.js +0 -93
  256. package/dist/__tests__/utils/agents.test.js.map +0 -1
  257. package/dist/capacity/claude.d.ts +0 -17
  258. package/dist/capacity/claude.d.ts.map +0 -1
  259. package/dist/capacity/claude.js.map +0 -1
  260. package/dist/capacity/codex.d.ts.map +0 -1
  261. package/dist/capacity/codex.js.map +0 -1
  262. package/dist/capacity/openai.d.ts +0 -47
  263. package/dist/capacity/openai.d.ts.map +0 -1
  264. package/dist/capacity/openai.js +0 -307
  265. package/dist/capacity/openai.js.map +0 -1
  266. package/dist/capacity/wham.d.ts +0 -16
  267. package/dist/capacity/wham.d.ts.map +0 -1
  268. package/dist/capacity/wham.js +0 -66
  269. package/dist/capacity/wham.js.map +0 -1
  270. package/dist/capacity/zai.d.ts +0 -14
  271. package/dist/capacity/zai.d.ts.map +0 -1
  272. package/dist/capacity/zai.js.map +0 -1
  273. package/dist/readiness/AgentReadiness.d.ts.map +0 -1
  274. package/dist/readiness/AgentReadiness.js +0 -614
  275. package/dist/readiness/AgentReadiness.js.map +0 -1
  276. package/dist/terminal/TmuxManager.d.ts +0 -28
  277. package/dist/terminal/TmuxManager.d.ts.map +0 -1
  278. package/dist/terminal/TmuxManager.js.map +0 -1
  279. package/dist/terminal/index.d.ts +0 -5
  280. package/dist/terminal/index.d.ts.map +0 -1
  281. package/dist/terminal/index.js +0 -5
  282. package/dist/terminal/index.js.map +0 -1
  283. package/dist/utils/agents.d.ts +0 -16
  284. package/dist/utils/agents.d.ts.map +0 -1
  285. package/dist/utils/agents.js +0 -117
  286. package/dist/utils/agents.js.map +0 -1
  287. package/dist/utils/index.d.ts +0 -8
  288. package/dist/utils/index.d.ts.map +0 -1
  289. package/dist/utils/index.js +0 -6
  290. package/dist/utils/index.js.map +0 -1
  291. package/src/__tests__/utils/agents.test.ts +0 -105
  292. package/src/capacity/openai.ts +0 -355
  293. package/src/readiness/AgentReadiness.ts +0 -730
  294. package/src/terminal/index.ts +0 -4
  295. package/src/utils/index.ts +0 -14
@@ -1,16 +1,11 @@
1
- import { readFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
1
  import type {
5
2
  Availability,
6
3
  CapacityLimitType,
7
- CapacityReport,
8
4
  CapacityWindow,
9
- } from "./types.js";
5
+ ProviderCapacitySnapshot,
6
+ } from "../types.js";
10
7
 
11
8
  const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
12
- const MISSING_API_KEY_MESSAGE =
13
- "z.ai API key not found; set Z_AI_API_KEY or configure Pi provider zai";
14
9
  const LIMIT_METADATA = {
15
10
  TOKENS_LIMIT: { id: "tokens", label: "Tokens" },
16
11
  CREDIT_LIMIT: { id: "credit", label: "Credit" },
@@ -24,13 +19,7 @@ const UNIT_METADATA = new Map<number, { minutes: number; label: string }>([
24
19
  ]);
25
20
  type UnknownRecord = Record<string, unknown>;
26
21
 
27
- export type ZaiCredentialOptions = {
28
- env?: NodeJS.ProcessEnv;
29
- readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
30
- };
31
-
32
- export type ZaiProbeOptions = ZaiCredentialOptions & {
33
- checkedAt: string;
22
+ export type ZaiRequestOptions = {
34
23
  fetch?: typeof globalThis.fetch;
35
24
  timeoutMs?: number;
36
25
  };
@@ -41,10 +30,6 @@ function record(value: unknown): UnknownRecord | null {
41
30
  : null;
42
31
  }
43
32
 
44
- function nonEmptyText(value: unknown): string | null {
45
- return typeof value === "string" && value.trim() ? value.trim() : null;
46
- }
47
-
48
33
  function optionalNumber(value: unknown, field: string): number | null {
49
34
  if (value === undefined || value === null) return null;
50
35
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -53,10 +38,6 @@ function optionalNumber(value: unknown, field: string): number | null {
53
38
  return value;
54
39
  }
55
40
 
56
- function clampPercent(value: number): number {
57
- return Math.max(0, Math.min(100, value));
58
- }
59
-
60
41
  function integer(value: unknown): number | null {
61
42
  return typeof value === "number" && Number.isInteger(value) ? value : null;
62
43
  }
@@ -65,50 +46,33 @@ function isCapacityLimitType(value: string): value is CapacityLimitType {
65
46
  return Object.hasOwn(LIMIT_METADATA, value);
66
47
  }
67
48
 
68
- function durationMinutes(
69
- unit: number,
70
- number: number,
71
- type: CapacityLimitType,
72
- ): number | null {
73
- if (type === "TIME_LIMIT" && unit === 5 && number === 1) return 30 * 24 * 60;
49
+ function durationMinutes(unit: number, count: number, type: CapacityLimitType): number | null {
50
+ if (type === "TIME_LIMIT" && unit === 5 && count === 1) return 30 * 24 * 60;
74
51
  const metadata = UNIT_METADATA.get(unit);
75
- return metadata === undefined || number <= 0
76
- ? null
77
- : metadata.minutes * number;
52
+ return metadata === undefined || count <= 0 ? null : metadata.minutes * count;
78
53
  }
79
54
 
80
- function windowLabel(
81
- type: CapacityLimitType,
82
- unit: number,
83
- number: number,
84
- ): string {
85
- if (type === "TIME_LIMIT" && unit === 5 && number === 1)
86
- return "MCP · monthly";
55
+ function windowLabel(type: CapacityLimitType, unit: number, count: number): string {
56
+ if (type === "TIME_LIMIT" && unit === 5 && count === 1) return "MCP · monthly";
87
57
  const kind = LIMIT_METADATA[type].label;
88
58
  const unitLabel = UNIT_METADATA.get(unit)?.label;
89
59
  if (!unitLabel) return kind;
90
- return `${kind} · ${number} ${unitLabel}${number === 1 ? "" : "s"}`;
60
+ return `${kind} · ${count} ${unitLabel}${count === 1 ? "" : "s"}`;
91
61
  }
92
62
 
93
63
  function resetTime(value: unknown): string | null {
94
64
  const milliseconds = optionalNumber(value, "limit nextResetTime");
95
65
  if (milliseconds === null) return null;
96
66
  const date = new Date(milliseconds);
97
- if (Number.isNaN(date.getTime()))
98
- throw new Error("Invalid z.ai quota limit nextResetTime");
67
+ if (Number.isNaN(date.getTime())) throw new Error("Invalid z.ai quota limit nextResetTime");
99
68
  return date.toISOString();
100
69
  }
101
70
 
102
71
  function availability(windows: CapacityWindow[]): Availability {
103
72
  if (windows.length === 0) return "unknown";
104
- if (
105
- windows.some(
106
- (window) => window.usedPercent !== null && window.usedPercent < 100,
107
- )
108
- ) {
109
- return "yes";
110
- }
111
- return "no";
73
+ return windows.some((window) => window.usedPercent !== null && window.usedPercent < 100)
74
+ ? "yes"
75
+ : "no";
112
76
  }
113
77
 
114
78
  function parseLimit(value: unknown, index: number): CapacityWindow | null {
@@ -120,29 +84,25 @@ function parseLimit(value: unknown, index: number): CapacityWindow | null {
120
84
  throw new Error("Invalid z.ai quota limit entry");
121
85
  }
122
86
  const serverPercent = optionalNumber(limit.percentage, "limit percentage");
123
- if (serverPercent === null)
124
- throw new Error("Invalid z.ai quota limit percentage");
87
+ if (serverPercent === null) throw new Error("Invalid z.ai quota limit percentage");
125
88
  if (!isCapacityLimitType(limit.type)) return null;
126
89
 
127
90
  const total = optionalNumber(limit.usage, "limit usage");
128
91
  const current = optionalNumber(limit.currentValue, "limit currentValue");
129
92
  const remaining = optionalNumber(limit.remaining, "limit remaining");
130
- let consumed =
131
- total !== null && remaining !== null ? total - remaining : null;
93
+ let consumed = total !== null && remaining !== null ? total - remaining : null;
132
94
  if (current !== null) consumed = Math.max(consumed ?? current, current);
133
-
134
95
  let usedPercent = serverPercent;
135
96
  if (total !== null && total > 0 && consumed !== null) {
136
97
  usedPercent = (consumed / total) * 100;
137
98
  }
138
99
  const metadata = LIMIT_METADATA[limit.type];
139
-
140
100
  return {
141
101
  id: `zai:${metadata.id}:${index + 1}`,
142
102
  label: windowLabel(limit.type, unit, count),
143
103
  limitType: limit.type,
144
104
  durationMinutes: durationMinutes(unit, count, limit.type),
145
- usedPercent: clampPercent(usedPercent),
105
+ usedPercent: Math.max(0, Math.min(100, usedPercent)),
146
106
  resetsAt: resetTime(limit.nextResetTime),
147
107
  total,
148
108
  current,
@@ -150,55 +110,16 @@ function parseLimit(value: unknown, index: number): CapacityWindow | null {
150
110
  };
151
111
  }
152
112
 
153
- export async function resolveZaiApiKey(
154
- options: ZaiCredentialOptions = {},
155
- ): Promise<string> {
156
- const env = options.env ?? process.env;
157
- const environmentKey = nonEmptyText(env.Z_AI_API_KEY);
158
- if (environmentKey) return environmentKey;
159
-
160
- const authPath = join(env.HOME || homedir(), ".pi", "agent", "auth.json");
161
- let parsed: UnknownRecord;
162
- try {
163
- const contents = await (options.readFile ?? readFile)(authPath, "utf8");
164
- const root = record(JSON.parse(contents));
165
- if (!root) throw new Error("invalid");
166
- parsed = root;
167
- } catch (error) {
168
- if (record(error)?.code === "ENOENT") {
169
- throw new Error(MISSING_API_KEY_MESSAGE);
170
- }
171
- throw new Error("z.ai Pi auth file is malformed");
172
- }
173
-
174
- const credential = record(parsed.zai);
175
- if (!credential) {
176
- throw new Error(MISSING_API_KEY_MESSAGE);
177
- }
178
- const key =
179
- credential.type === "api_key" ? nonEmptyText(credential.key) : null;
180
- if (!key) throw new Error("z.ai Pi auth file has a malformed zai credential");
181
- return key;
182
- }
183
-
184
- export function parseZaiQuota(raw: unknown, checkedAt: string): CapacityReport {
113
+ export function parseZaiCapacity(raw: unknown): ProviderCapacitySnapshot {
185
114
  const root = record(raw);
186
115
  const data = record(root?.data);
187
- if (
188
- root?.success !== true ||
189
- root.code !== 200 ||
190
- !data ||
191
- !Array.isArray(data.limits)
192
- ) {
116
+ if (root?.success !== true || root.code !== 200 || !data || !Array.isArray(data.limits)) {
193
117
  throw new Error("Invalid z.ai quota response");
194
118
  }
195
119
  const windows = data.limits
196
120
  .map(parseLimit)
197
121
  .filter((window): window is CapacityWindow => window !== null);
198
122
  return {
199
- harness: "pi",
200
- provider: "zai",
201
- generatedAt: checkedAt,
202
123
  authenticated: true,
203
124
  available: availability(windows),
204
125
  windows,
@@ -206,32 +127,29 @@ export function parseZaiQuota(raw: unknown, checkedAt: string): CapacityReport {
206
127
  };
207
128
  }
208
129
 
209
- export async function probeZaiCapacity(
210
- options: ZaiProbeOptions,
211
- ): Promise<CapacityReport> {
212
- const key = await resolveZaiApiKey(options);
213
- const controller = new AbortController();
214
- const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5000);
130
+ export async function fetchZaiCapacity(
131
+ key: string,
132
+ options: ZaiRequestOptions = {},
133
+ ): Promise<ProviderCapacitySnapshot> {
215
134
  let response: Response;
216
135
  try {
217
136
  response = await (options.fetch ?? globalThis.fetch)(ZAI_QUOTA_URL, {
218
137
  method: "GET",
219
138
  headers: { Authorization: `Bearer ${key}` },
220
- signal: controller.signal,
139
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5000),
221
140
  });
222
141
  } catch {
223
142
  throw new Error("z.ai quota request failed");
224
- } finally {
225
- clearTimeout(timer);
226
143
  }
227
144
  if (response.status !== 200) {
228
145
  throw new Error(`z.ai quota request failed: HTTP ${response.status}`);
229
146
  }
230
- let raw: unknown;
231
147
  try {
232
- raw = await response.json();
233
- } catch {
234
- throw new Error("z.ai quota response is not valid JSON");
148
+ return parseZaiCapacity(await response.json());
149
+ } catch (error) {
150
+ if (error instanceof SyntaxError) {
151
+ throw new Error("z.ai quota response is not valid JSON");
152
+ }
153
+ throw error;
235
154
  }
236
- return parseZaiQuota(raw, options.checkedAt);
237
155
  }
@@ -0,0 +1,30 @@
1
+ import {
2
+ fetchAnthropicCapacity,
3
+ type AnthropicCapacityRequestOptions,
4
+ } from "../providers/anthropic.js";
5
+ import type { CapacityReport, ProviderCapacitySnapshot } from "../types.js";
6
+ import {
7
+ resolveClaudeAnthropicToken,
8
+ type ClaudeCredentialOptions,
9
+ } from "../../harnesses/claude/credentials.js";
10
+
11
+ export type ClaudeCapacityOptions = ClaudeCredentialOptions & AnthropicCapacityRequestOptions;
12
+
13
+ export type ClaudeProbeOptions = ClaudeCapacityOptions & {
14
+ checkedAt: string;
15
+ };
16
+
17
+ function claudeReport(snapshot: ProviderCapacitySnapshot, checkedAt: string): CapacityReport {
18
+ return {
19
+ harness: "claude",
20
+ provider: "anthropic",
21
+ generatedAt: checkedAt,
22
+ ...snapshot,
23
+ };
24
+ }
25
+
26
+ export async function probeClaudeCapacity(options: ClaudeProbeOptions): Promise<CapacityReport> {
27
+ const token = await resolveClaudeAnthropicToken(options);
28
+ const snapshot = await fetchAnthropicCapacity(token, options);
29
+ return claudeReport(snapshot, options.checkedAt);
30
+ }
@@ -1,10 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { parseWhamUsage, resetTime, safeIdentifier } from "./wham.js";
5
- import type { CapacityReport, CapacityWindow } from "./types.js";
6
-
7
- export { toRateWindow } from "./wham.js";
2
+ import {
3
+ fetchOpenAiPatAccountId,
4
+ fetchOpenAiSubscriptionCapacity,
5
+ parseWhamUsage,
6
+ resetTime,
7
+ safeIdentifier,
8
+ } from "../providers/openai-subscription.js";
9
+ import type { CapacityReport, CapacityWindow } from "../types.js";
10
+ import {
11
+ resolveCodexCredentials,
12
+ type CodexCredentialOptions,
13
+ } from "../../harnesses/codex/credentials.js";
8
14
 
9
15
  type CodexUsageSource = "pat" | "oauth" | "cli";
10
16
  type UsageSnapshot = {
@@ -18,19 +24,16 @@ type RpcMessage = { id?: number; method: string; params?: UnknownRecord };
18
24
  type CliResponses = { rateLimits: unknown; account: unknown };
19
25
  type CodexRpc = (messages: RpcMessage[]) => Promise<CliResponses>;
20
26
 
21
- export const CODEX_APP_SERVER_ARGS = ["-s", "read-only", "-a", "untrusted", "app-server"] as const;
22
-
23
- type CodexProbeOptions = {
27
+ export type CodexProbeOptions = CodexCredentialOptions & {
24
28
  installed: boolean;
25
29
  checkedAt: string;
26
- readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
27
30
  fetch?: typeof globalThis.fetch;
28
31
  rpc?: CodexRpc;
29
32
  timeoutMs?: number;
30
- env?: NodeJS.ProcessEnv;
31
- now?: () => Date;
32
33
  };
33
34
 
35
+ export const CODEX_APP_SERVER_ARGS = ["-s", "read-only", "-a", "untrusted", "app-server"] as const;
36
+
34
37
  function record(value: unknown): UnknownRecord | null {
35
38
  return value !== null && typeof value === "object" && !Array.isArray(value)
36
39
  ? (value as UnknownRecord)
@@ -45,11 +48,6 @@ function nonEmptyText(value: unknown): string | null {
45
48
  return typeof value === "string" && value.length > 0 ? value : null;
46
49
  }
47
50
 
48
- export function resolveCodexAuthPath(env: NodeJS.ProcessEnv = process.env): string {
49
- const root = env.CODEX_HOME || join(env.HOME || "", ".codex");
50
- return join(root, "auth.json");
51
- }
52
-
53
51
  export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot {
54
52
  const { windows, creditsRemaining } = parseWhamUsage(raw);
55
53
  return { windows, creditsRemaining, source };
@@ -77,14 +75,15 @@ function cliSnapshotWindows(value: unknown, fallbackId: string): CapacityWindow[
77
75
  ].filter((item): item is CapacityWindow => item !== null);
78
76
  }
79
77
 
80
- export function parseCliUsage(raw: unknown): UsageSnapshot {
78
+ function parseCliUsage(raw: unknown): UsageSnapshot {
81
79
  const response = record(raw) ?? {};
82
80
  const primary = record(response.rateLimits);
83
81
  const windows = primary ? cliSnapshotWindows(primary, "codex") : [];
84
82
  const buckets = record(response.rateLimitsByLimitId);
85
83
  if (buckets) {
86
- for (const [id, snapshot] of Object.entries(buckets))
84
+ for (const [id, snapshot] of Object.entries(buckets)) {
87
85
  windows.push(...cliSnapshotWindows(snapshot, id));
86
+ }
88
87
  }
89
88
  const unique = [...new Map(windows.map((window) => [window.id, window])).values()];
90
89
  return { windows: unique, creditsRemaining: null, source: "cli" };
@@ -92,7 +91,7 @@ export function parseCliUsage(raw: unknown): UsageSnapshot {
92
91
 
93
92
  function capacityFromSnapshot(
94
93
  snapshot: UsageSnapshot,
95
- context: CodexProbeOptions,
94
+ options: CodexProbeOptions,
96
95
  raw?: unknown,
97
96
  ): CapacityReport {
98
97
  const hasUsage = snapshot.windows.some((window) => window.usedPercent !== null);
@@ -103,7 +102,7 @@ function capacityFromSnapshot(
103
102
  return {
104
103
  harness: "codex",
105
104
  provider: "openai",
106
- generatedAt: context.checkedAt,
105
+ generatedAt: options.checkedAt,
107
106
  authenticated: true,
108
107
  available: reached ? "no" : hasUsage ? "yes" : "unknown",
109
108
  windows: snapshot.windows,
@@ -111,70 +110,9 @@ function capacityFromSnapshot(
111
110
  };
112
111
  }
113
112
 
114
- function jwtExpiry(token: string): number | null {
115
- const part = token.split(".")[1];
116
- if (!part) return null;
117
- try {
118
- return finiteNumber(record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp);
119
- } catch {
120
- return null;
121
- }
122
- }
123
-
124
- function staleOAuth(tokens: UnknownRecord, token: string, now: Date): boolean {
125
- const metadata = tokens.expires_at ?? tokens.expiresAt ?? tokens.expiry;
126
- let expiry: number | null = finiteNumber(metadata);
127
- if (typeof metadata === "string") {
128
- const parsed = Date.parse(metadata);
129
- expiry = Number.isNaN(parsed) ? null : parsed / 1000;
130
- }
131
- expiry ??= jwtExpiry(token);
132
- return expiry !== null && expiry <= now.getTime() / 1000;
133
- }
134
-
135
- async function fetchJson(
136
- fetcher: typeof globalThis.fetch,
137
- url: string,
138
- init: RequestInit,
139
- timeoutMs: number,
140
- ): Promise<unknown> {
141
- const controller = new AbortController();
142
- const timer = setTimeout(() => controller.abort(), timeoutMs);
143
- try {
144
- const response = await fetcher(url, { ...init, signal: controller.signal });
145
- if (!response.ok) throw new Error(response.status === 401 ? "unauthorized" : "request failed");
146
- return await response.json();
147
- } finally {
148
- clearTimeout(timer);
149
- }
150
- }
151
-
152
- async function apiSnapshot(
153
- token: string,
154
- accountId: string,
155
- source: "pat" | "oauth",
156
- options: CodexProbeOptions,
157
- ): Promise<UsageSnapshot> {
158
- const fetcher = options.fetch ?? globalThis.fetch;
159
- const raw = await fetchJson(
160
- fetcher,
161
- "https://chatgpt.com/backend-api/wham/usage",
162
- {
163
- headers: {
164
- Authorization: `Bearer ${token}`,
165
- "ChatGPT-Account-Id": accountId,
166
- },
167
- },
168
- options.timeoutMs ?? 5000,
169
- );
170
- return parseUsage(raw, source);
171
- }
172
-
173
113
  function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResponses> {
174
114
  return new Promise((resolve, reject) => {
175
- const child = spawn("codex", CODEX_APP_SERVER_ARGS, {
176
- stdio: ["pipe", "pipe", "ignore"],
177
- });
115
+ const child = spawn("codex", CODEX_APP_SERVER_ARGS, { stdio: ["pipe", "pipe", "ignore"] });
178
116
  const results: Partial<CliResponses> = {};
179
117
  let buffer = "";
180
118
  let settled = false;
@@ -235,12 +173,8 @@ export function codexUnavailableReport(checkedAt: string): CapacityReport {
235
173
  };
236
174
  }
237
175
 
238
- function unavailable(options: CodexProbeOptions): CapacityReport {
239
- return codexUnavailableReport(options.checkedAt);
240
- }
241
-
242
176
  async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport> {
243
- if (!options.installed) return unavailable(options);
177
+ if (!options.installed) return codexUnavailableReport(options.checkedAt);
244
178
  const messages: RpcMessage[] = [
245
179
  {
246
180
  id: 1,
@@ -273,57 +207,53 @@ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport>
273
207
  }
274
208
  return result;
275
209
  } catch {
276
- return unavailable(options);
210
+ return codexUnavailableReport(options.checkedAt);
277
211
  }
278
212
  }
279
213
 
214
+ async function subscriptionSnapshot(
215
+ access: string,
216
+ accountId: string,
217
+ source: "pat" | "oauth",
218
+ options: CodexProbeOptions,
219
+ ): Promise<UsageSnapshot> {
220
+ const snapshot = await fetchOpenAiSubscriptionCapacity({ access, accountId }, options);
221
+ if (snapshot.authenticated === false) throw new Error("unauthorized");
222
+ return {
223
+ windows: snapshot.windows,
224
+ creditsRemaining: snapshot.creditsRemaining,
225
+ source,
226
+ };
227
+ }
228
+
280
229
  export async function probeCodexCapacity(options: CodexProbeOptions): Promise<CapacityReport> {
281
- let parsed: UnknownRecord | null = null;
282
- try {
283
- const contents = await (options.readFile ?? readFile)(
284
- resolveCodexAuthPath(options.env),
285
- "utf8",
286
- );
287
- parsed = record(JSON.parse(contents));
288
- } catch {
289
- return cliFallback(options);
290
- }
230
+ const credentials = await resolveCodexCredentials(options);
231
+ if (!credentials) return cliFallback(options);
291
232
 
292
- const auth = parsed ?? {};
293
- const pat = nonEmptyText(auth.personal_access_token);
294
- if (pat) {
233
+ if (credentials.personalAccessToken) {
295
234
  try {
296
- const fetcher = options.fetch ?? globalThis.fetch;
297
- const whoami = record(
298
- await fetchJson(
299
- fetcher,
300
- "https://auth.openai.com/api/accounts/v1/user-auth-credential/whoami",
301
- { headers: { Authorization: `Bearer ${pat}` } },
302
- options.timeoutMs ?? 5000,
303
- ),
235
+ const accountId = await fetchOpenAiPatAccountId(credentials.personalAccessToken, options);
236
+ const snapshot = await subscriptionSnapshot(
237
+ credentials.personalAccessToken,
238
+ accountId,
239
+ "pat",
240
+ options,
304
241
  );
305
- const accountId = nonEmptyText(whoami?.chatgpt_account_id);
306
- if (!accountId) throw new Error("account unavailable");
307
- return capacityFromSnapshot(await apiSnapshot(pat, accountId, "pat", options), options);
242
+ return capacityFromSnapshot(snapshot, options);
308
243
  } catch {
309
244
  // Continue to a separately available OAuth credential before using the CLI.
310
245
  }
311
246
  }
312
247
 
313
- const tokens = record(auth.tokens);
314
- const accessToken = nonEmptyText(tokens?.access_token);
315
- const accountId = nonEmptyText(tokens?.account_id);
316
- if (
317
- tokens &&
318
- accessToken &&
319
- accountId &&
320
- !staleOAuth(tokens, accessToken, (options.now ?? (() => new Date()))())
321
- ) {
248
+ if (credentials.oauth) {
322
249
  try {
323
- return capacityFromSnapshot(
324
- await apiSnapshot(accessToken, accountId, "oauth", options),
250
+ const snapshot = await subscriptionSnapshot(
251
+ credentials.oauth.access,
252
+ credentials.oauth.accountId,
253
+ "oauth",
325
254
  options,
326
255
  );
256
+ return capacityFromSnapshot(snapshot, options);
327
257
  } catch {
328
258
  return cliFallback(options);
329
259
  }
@@ -0,0 +1,131 @@
1
+ import { fetchAnthropicCapacity } from "../providers/anthropic.js";
2
+ import { fetchOpenAiPlatformCapacity } from "../providers/openai-platform.js";
3
+ import { fetchOpenAiSubscriptionCapacity } from "../providers/openai-subscription.js";
4
+ import { fetchZaiCapacity } from "../providers/zai.js";
5
+ import type { CapacityReport, ProviderCapacitySnapshot } from "../types.js";
6
+ import {
7
+ resolvePiAnthropicCredential,
8
+ resolvePiOpenAiCredential,
9
+ resolvePiZaiApiKey,
10
+ type PiCredentialOptions,
11
+ type PiOpenAiCredential,
12
+ } from "../../harnesses/pi/credentials.js";
13
+
14
+ export type PiAnthropicCapacityOptions = PiCredentialOptions & {
15
+ now?: () => Date;
16
+ fetch?: typeof globalThis.fetch;
17
+ timeoutMs?: number;
18
+ };
19
+
20
+ export type PiAnthropicProbeOptions = PiAnthropicCapacityOptions & {
21
+ checkedAt: string;
22
+ };
23
+
24
+ export type PiOpenAiCapacityOptions = PiCredentialOptions & {
25
+ now?: () => Date;
26
+ fetch?: typeof globalThis.fetch;
27
+ timeoutMs?: number;
28
+ };
29
+
30
+ export type PiOpenAiProbeOptions = PiOpenAiCapacityOptions & {
31
+ checkedAt: string;
32
+ };
33
+
34
+ export type PiZaiCapacityOptions = PiCredentialOptions & {
35
+ now?: () => Date;
36
+ fetch?: typeof globalThis.fetch;
37
+ timeoutMs?: number;
38
+ };
39
+
40
+ export type PiZaiProbeOptions = PiZaiCapacityOptions & {
41
+ checkedAt: string;
42
+ };
43
+
44
+ export async function probePiAnthropicCapacity(
45
+ options: PiAnthropicProbeOptions,
46
+ ): Promise<CapacityReport> {
47
+ const credential = await resolvePiAnthropicCredential(options);
48
+ const now = options.now?.() ?? new Date();
49
+ if (credential.expiresMs !== null && credential.expiresMs <= now.getTime()) {
50
+ return {
51
+ harness: "pi",
52
+ provider: "anthropic",
53
+ generatedAt: options.checkedAt,
54
+ authenticated: true,
55
+ available: "unknown",
56
+ windows: [],
57
+ creditsRemaining: null,
58
+ };
59
+ }
60
+ const snapshot = await fetchAnthropicCapacity(credential.access, options);
61
+ return {
62
+ harness: "pi",
63
+ provider: "anthropic",
64
+ generatedAt: options.checkedAt,
65
+ ...snapshot,
66
+ };
67
+ }
68
+
69
+ function piOpenAiReport(snapshot: ProviderCapacitySnapshot, checkedAt: string): CapacityReport {
70
+ return {
71
+ harness: "pi",
72
+ provider: "openai",
73
+ generatedAt: checkedAt,
74
+ ...snapshot,
75
+ };
76
+ }
77
+
78
+ function jwtExpiryMs(token: string): number | null {
79
+ const part = token.split(".")[1];
80
+ if (!part) return null;
81
+ try {
82
+ const payload = JSON.parse(Buffer.from(part, "base64url").toString("utf8")) as unknown;
83
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
84
+ const exp = (payload as Record<string, unknown>).exp;
85
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1000 : null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function oauthExpired(
92
+ credential: Extract<PiOpenAiCredential, { kind: "oauth" }>,
93
+ nowMs: number,
94
+ ): boolean {
95
+ const expiryMs = credential.expiresMs ?? jwtExpiryMs(credential.access);
96
+ return expiryMs !== null && expiryMs <= nowMs;
97
+ }
98
+
99
+ export async function probeOpenAiCapacity(options: PiOpenAiProbeOptions): Promise<CapacityReport> {
100
+ const credential = await resolvePiOpenAiCredential(options);
101
+ if (credential.kind === "platform") {
102
+ const snapshot = await fetchOpenAiPlatformCapacity(credential.key, options);
103
+ return piOpenAiReport(snapshot, options.checkedAt);
104
+ }
105
+
106
+ const nowMs = Date.parse(options.checkedAt);
107
+ if (Number.isNaN(nowMs) || oauthExpired(credential, nowMs)) {
108
+ return piOpenAiReport(
109
+ {
110
+ authenticated: true,
111
+ available: "unknown",
112
+ windows: [],
113
+ creditsRemaining: null,
114
+ },
115
+ options.checkedAt,
116
+ );
117
+ }
118
+ const snapshot = await fetchOpenAiSubscriptionCapacity(credential, options);
119
+ return piOpenAiReport(snapshot, options.checkedAt);
120
+ }
121
+
122
+ export async function probeZaiCapacity(options: PiZaiProbeOptions): Promise<CapacityReport> {
123
+ const key = await resolvePiZaiApiKey(options);
124
+ const snapshot = await fetchZaiCapacity(key, options);
125
+ return {
126
+ harness: "pi",
127
+ provider: "zai",
128
+ generatedAt: options.checkedAt,
129
+ ...snapshot,
130
+ };
131
+ }
@@ -24,3 +24,6 @@ export interface CapacityReport {
24
24
  windows: CapacityWindow[];
25
25
  creditsRemaining: number | null;
26
26
  }
27
+
28
+ /** Provider quota data before a harness/source identity is attached. */
29
+ export type ProviderCapacitySnapshot = Omit<CapacityReport, "harness" | "provider" | "generatedAt">;