@xingular/harness 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +28 -25
  3. package/dist/cli.js +6675 -2501
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +15 -1
  11. package/dist/sdk/agents/planResearch.d.ts +5 -0
  12. package/dist/sdk/agents/planResearch.js +19 -0
  13. package/dist/sdk/agents/profiles.d.ts +2 -1
  14. package/dist/sdk/agents/profiles.js +9 -4
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/cli.js +5 -4
  26. package/dist/sdk/commands/list.d.ts +3 -0
  27. package/dist/sdk/commands/list.js +75 -0
  28. package/dist/sdk/commands/registry.d.ts +250 -0
  29. package/dist/sdk/commands/registry.js +47 -0
  30. package/dist/sdk/commands/routes.d.ts +16 -0
  31. package/dist/sdk/commands/routes.js +33 -0
  32. package/dist/sdk/core/commandBudget.d.ts +23 -0
  33. package/dist/sdk/core/commandBudget.js +57 -0
  34. package/dist/sdk/core/index.js +17 -1
  35. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  36. package/dist/sdk/core/interactiveProfile.js +143 -0
  37. package/dist/sdk/core/workspace.d.ts +13 -0
  38. package/dist/sdk/core/workspace.js +88 -0
  39. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  40. package/dist/sdk/core/workspaceBashTool.js +36 -0
  41. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  42. package/dist/sdk/core/workspaceSandbox.js +320 -0
  43. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  44. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  45. package/dist/sdk/doctor/types.d.ts +2 -2
  46. package/dist/sdk/harness.d.ts +7 -0
  47. package/dist/sdk/harness.js +42 -2
  48. package/dist/sdk/index.d.ts +6 -1
  49. package/dist/sdk/index.js +3 -0
  50. package/dist/sdk/pi/runtime.d.ts +2 -1
  51. package/dist/sdk/pi/runtime.js +2 -2
  52. package/dist/sdk/plugins/builtin.js +29 -12
  53. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  54. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  55. package/dist/sdk/plugins/catalogue.js +2 -1
  56. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  57. package/dist/sdk/plugins/commandPrefix.js +21 -0
  58. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  59. package/dist/sdk/plugins/firstParty.js +8 -2
  60. package/dist/sdk/plugins/inventory.js +4 -3
  61. package/dist/sdk/plugins/manifest.d.ts +2 -0
  62. package/dist/sdk/plugins/manifest.js +2 -1
  63. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  64. package/dist/sdk/plugins/surfaces.js +11 -0
  65. package/dist/sdk/plugins/types.d.ts +15 -0
  66. package/dist/sdk/prompt/compile.d.ts +14 -0
  67. package/dist/sdk/prompt/compile.js +65 -29
  68. package/dist/sdk/prompt/standard.d.ts +17 -0
  69. package/dist/sdk/prompt/standard.js +21 -0
  70. package/dist/sdk/server/methods.js +18 -0
  71. package/dist/sdk/server/remote.js +36 -11
  72. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  73. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  74. package/dist/sdk/sessions/continuation.d.ts +3 -0
  75. package/dist/sdk/sessions/continuation.js +5 -1
  76. package/dist/sdk/sessions/contracts.d.ts +168 -0
  77. package/dist/sdk/sessions/contracts.js +59 -1
  78. package/dist/sdk/sessions/control.d.ts +72 -0
  79. package/dist/sdk/sessions/control.js +158 -0
  80. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  81. package/dist/sdk/sessions/initialBash.js +7 -0
  82. package/dist/sdk/sessions/planMode.d.ts +14 -3
  83. package/dist/sdk/sessions/planMode.js +73 -16
  84. package/dist/sdk/sessions/registry.d.ts +1 -1
  85. package/dist/sdk/sessions/registry.js +2 -2
  86. package/dist/sdk/sessions/runStore.d.ts +14 -0
  87. package/dist/sdk/sessions/runStore.js +35 -0
  88. package/dist/sdk/sessions/runner.d.ts +40 -2
  89. package/dist/sdk/sessions/runner.js +161 -13
  90. package/dist/sdk/sessions/sessions.d.ts +23 -3
  91. package/dist/sdk/sessions/sessions.js +202 -17
  92. package/dist/sdk/sessions/subagentService.d.ts +9 -0
  93. package/dist/sdk/sessions/subagentService.js +63 -4
  94. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  95. package/dist/sdk/sessions/workSlice.js +297 -0
  96. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  97. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  98. package/dist/sdk/terminal/browserControl.js +2 -1
  99. package/dist/sdk/terminal/controller.d.ts +11 -3
  100. package/dist/sdk/terminal/controller.js +40 -13
  101. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  102. package/dist/sdk/terminal/opentui/theme.js +87 -0
  103. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  104. package/dist/sdk/terminal/piExtension.js +154 -54
  105. package/dist/sdk/terminal/standalone.d.ts +7 -0
  106. package/dist/sdk/terminal/standalone.js +72 -13
  107. package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
  108. package/dist/sdk/terminal/subagentMonitor.js +32 -1
  109. package/dist/sdk/terminal/terminals.d.ts +19 -2
  110. package/dist/sdk/terminal/terminals.js +245 -35
  111. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  112. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  113. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  114. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  115. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  116. package/dist/sdk/terminal/tui/commands.js +205 -124
  117. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  118. package/dist/sdk/terminal/tui/list.js +96 -14
  119. package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
  120. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  121. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  122. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  124. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  125. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  126. package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
  127. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  128. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  129. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  130. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  131. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  132. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  133. package/dist/sdk/terminal/tui/runtime.js +23 -3
  134. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  135. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  136. package/dist/sdk/tools/checklist.js +1 -0
  137. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  138. package/dist/sdk/tools/checkpoint.js +154 -0
  139. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  140. package/dist/sdk/tools/getCapabilities.js +34 -0
  141. package/dist/sdk/tools/spawnSubagent.js +4 -3
  142. package/dist/sdk/usage/analytics.d.ts +20 -0
  143. package/dist/sdk/usage/analytics.js +151 -0
  144. package/dist/sdk/usage/ledger.d.ts +38 -0
  145. package/dist/sdk/usage/ledger.js +142 -0
  146. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  147. package/dist/sdk/usage/limits/adapters.js +349 -0
  148. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  149. package/dist/sdk/usage/limits/capture.js +47 -0
  150. package/dist/sdk/usage/limits/http.d.ts +12 -0
  151. package/dist/sdk/usage/limits/http.js +51 -0
  152. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  153. package/dist/sdk/usage/limits/normalize.js +211 -0
  154. package/dist/sdk/usage/limits/service.d.ts +30 -0
  155. package/dist/sdk/usage/limits/service.js +176 -0
  156. package/dist/sdk/usage/limits/types.d.ts +65 -0
  157. package/dist/sdk/usage/limits/types.js +1 -0
  158. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  159. package/dist/sdk/usage/serverMethods.js +22 -0
  160. package/dist/sdk/usage/service.d.ts +32 -0
  161. package/dist/sdk/usage/service.js +66 -0
  162. package/dist/sdk/usage/settings.d.ts +7 -0
  163. package/dist/sdk/usage/settings.js +28 -0
  164. package/dist/sdk/usage/types.d.ts +60 -0
  165. package/dist/sdk/usage/types.js +4 -0
  166. package/dist/terminal-opentui.js +5207 -1883
  167. package/dist/terminal-pi.js +1658 -271
  168. package/docs/architecture/02-sessions-and-runs.md +128 -26
  169. package/docs/architecture/04-prompt.md +50 -12
  170. package/docs/architecture/06-plugins-and-capabilities.md +24 -7
  171. package/docs/architecture/07-tools-and-gate.md +104 -24
  172. package/docs/architecture/08-memory-and-history.md +4 -2
  173. package/docs/architecture/09-accounts-and-connectors.md +31 -1
  174. package/docs/architecture/10-server-protocol.md +58 -3
  175. package/docs/architecture/11-terminal.md +105 -42
  176. package/docs/architecture/12-storage.md +30 -2
  177. package/docs/architecture/13-world-tools.md +22 -1
  178. package/docs/architecture/14-surfaces-and-parity.md +54 -7
  179. package/docs/architecture/README.md +3 -1
  180. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  181. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  182. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  183. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  184. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  185. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  186. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  187. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  188. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  189. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  190. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  191. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  192. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  193. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  194. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  195. package/docs/architecture/proposals/README.md +14 -0
  196. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  197. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  198. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  199. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  200. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  201. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  202. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  203. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  204. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  205. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  206. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  207. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  208. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  209. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  210. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  211. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  212. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  213. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  214. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  215. package/docs/parity.md +80 -24
  216. package/docs/releases/1.1.1.md +28 -0
  217. package/docs/releases/1.2.0.md +49 -0
  218. package/docs/releases/4.0.0.md +10 -9
  219. package/docs/releases/README.md +35 -30
  220. package/docs/study/00-index.md +3 -0
  221. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  222. package/docs/terminal-tui.md +100 -59
  223. package/package.json +4 -3
@@ -0,0 +1,142 @@
1
+ import { all, run as execute, transaction, withIndex } from "../core/index.js";
2
+ import { now } from "../core/fs.js";
3
+ import { readAllRuns } from "../sessions/runIndex.js";
4
+ import { findRun, readEvents } from "../sessions/runStore.js";
5
+ import { usagePayloadSchema } from "../sessions/contracts.js";
6
+ const number = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
7
+ const optionalNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
8
+ const text = (value) => (typeof value === 'string' && value ? value : null);
9
+ /** One row per request: the event's own `request`, or the run's totals when a provider reported only those. */
10
+ export function usageRowsFor(run, events) {
11
+ const rows = [];
12
+ let lastTotals = null;
13
+ let lastTotalsAt = null;
14
+ for (const event of events) {
15
+ if (event.type !== 'usage.updated')
16
+ continue;
17
+ const parsed = usagePayloadSchema.safeParse(event.payload);
18
+ if (!parsed.success)
19
+ continue;
20
+ const payload = parsed.data;
21
+ lastTotals = payload;
22
+ lastTotalsAt = event.timestamp;
23
+ const request = payload.request;
24
+ // Without a per-request update the event's fields are cumulative; only the last one may count.
25
+ if (!request)
26
+ continue;
27
+ rows.push({
28
+ runId: run.id,
29
+ sequence: event.sequence,
30
+ requestIndex: request?.index ?? payload.requests ?? rows.length + 1,
31
+ at: event.timestamp,
32
+ day: event.timestamp.slice(0, 10),
33
+ accountId: run.accountId ?? null,
34
+ projectId: run.projectId ?? null,
35
+ sessionId: run.sessionId,
36
+ agentId: run.agentId ?? null,
37
+ mode: run.mode ?? 'chat',
38
+ trigger: run.trigger ?? null,
39
+ provider: request?.provider ?? payload.provider ?? null,
40
+ model: request?.model ?? payload.model ?? null,
41
+ inputTokens: number(request?.inputTokens ?? payload.inputTokens),
42
+ cacheReadTokens: number(request?.cacheReadTokens ?? payload.cacheReadTokens),
43
+ cacheWriteTokens: number(request?.cacheWriteTokens ?? payload.cacheWriteTokens),
44
+ outputTokens: number(request?.outputTokens ?? payload.outputTokens),
45
+ reasoningTokens: number(request?.reasoningTokens ?? payload.reasoningTokens),
46
+ costUsd: optionalNumber(request?.costUsd ?? payload.costUsd),
47
+ costSource: text(request?.costSource ?? payload.costSource),
48
+ contextTokens: optionalNumber(payload.contextTokens),
49
+ contextWindow: optionalNumber(payload.contextWindow),
50
+ durationMs: optionalNumber(request?.durationMs),
51
+ ttftMs: optionalNumber(request?.ttftMs),
52
+ requests: number(payload.requests)
53
+ });
54
+ }
55
+ // A run whose provider reported totals but no per-request update (an older record) still counts once.
56
+ if (!rows.length && lastTotals) {
57
+ rows.push({
58
+ runId: run.id, sequence: 0, requestIndex: lastTotals.requests ?? 1,
59
+ at: lastTotalsAt ?? run.endedAt ?? run.startedAt, day: (lastTotalsAt ?? run.endedAt ?? run.startedAt).slice(0, 10),
60
+ accountId: run.accountId ?? null, projectId: run.projectId ?? null, sessionId: run.sessionId,
61
+ agentId: run.agentId ?? null, mode: run.mode ?? 'chat', trigger: run.trigger ?? null,
62
+ provider: lastTotals.provider ?? null, model: lastTotals.model ?? null,
63
+ inputTokens: number(lastTotals.inputTokens), cacheReadTokens: number(lastTotals.cacheReadTokens),
64
+ cacheWriteTokens: number(lastTotals.cacheWriteTokens), outputTokens: number(lastTotals.outputTokens),
65
+ reasoningTokens: number(lastTotals.reasoningTokens), costUsd: optionalNumber(lastTotals.costUsd),
66
+ costSource: text(lastTotals.costSource), contextTokens: optionalNumber(lastTotals.contextTokens),
67
+ contextWindow: optionalNumber(lastTotals.contextWindow), durationMs: null, ttftMs: null,
68
+ requests: number(lastTotals.requests)
69
+ });
70
+ }
71
+ return rows;
72
+ }
73
+ function write(database, run, rows) {
74
+ execute(database, 'DELETE FROM usage_requests WHERE run_id = ?', run.id);
75
+ for (const row of rows) {
76
+ execute(database, `INSERT INTO usage_requests (run_id, sequence, request_index, at, day, account_id, project_id, session_id, agent_id, mode, trigger,
77
+ provider, model, input_tokens, cache_read_tokens, cache_write_tokens, output_tokens, reasoning_tokens,
78
+ cost_usd, cost_source, context_tokens, context_window, duration_ms, ttft_ms, requests)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, row.runId, row.sequence, row.requestIndex, row.at, row.day, row.accountId, row.projectId, row.sessionId, row.agentId, row.mode, row.trigger, row.provider, row.model, row.inputTokens, row.cacheReadTokens, row.cacheWriteTokens, row.outputTokens, row.reasoningTokens, row.costUsd, row.costSource, row.contextTokens, row.contextWindow, row.durationMs, row.ttftMs, row.requests);
80
+ }
81
+ execute(database, 'INSERT OR REPLACE INTO usage_indexed (run_id, indexed_at, rows) VALUES (?, ?, ?)', run.id, now(), rows.length);
82
+ }
83
+ /** Indexes one run's usage. Idempotent, and safe to call when the index is unavailable. */
84
+ export function indexRunUsage(root, runId) {
85
+ const run = findRun(root, runId);
86
+ if (!run)
87
+ return false;
88
+ let events;
89
+ try {
90
+ events = readEvents(root, runId);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ const rows = usageRowsFor(run, events);
96
+ try {
97
+ withIndex(root, (database) => transaction(database, () => write(database, run, rows)));
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ /** Indexes every run the ledger has not seen yet. Returns how many runs it added. */
105
+ export function syncUsage(root) {
106
+ const indexed = new Set();
107
+ try {
108
+ withIndex(root, (database) => {
109
+ for (const row of all(database, 'SELECT run_id FROM usage_indexed'))
110
+ indexed.add(row.run_id);
111
+ });
112
+ }
113
+ catch {
114
+ return 0;
115
+ }
116
+ let added = 0;
117
+ for (const record of readAllRuns(root)) {
118
+ if (indexed.has(record.id))
119
+ continue;
120
+ if (indexRunUsage(root, record.id))
121
+ added += 1;
122
+ }
123
+ return added;
124
+ }
125
+ /** The ledger's last write, or null when it holds nothing. */
126
+ export function lastIndexedAt(root) {
127
+ try {
128
+ return withIndex(root, (database) => all(database, 'SELECT MAX(indexed_at) AS at FROM usage_indexed')[0]?.at ?? null);
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ /** Run ids the ledger knows, for diagnostics and tests. */
135
+ export function indexedRuns(root) {
136
+ try {
137
+ return withIndex(root, (database) => all(database, 'SELECT run_id FROM usage_indexed').map((row) => row.run_id));
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProviderLimitAdapter } from "./types.js";
2
+ export declare const OPENROUTER_ADAPTER: ProviderLimitAdapter;
3
+ export declare const DEEPSEEK_ADAPTER: ProviderLimitAdapter;
4
+ export declare const ANTHROPIC_ADMIN_ADAPTER: ProviderLimitAdapter;
5
+ /** ChatGPT/Codex plan windows and credits, from the account's own usage endpoint. */
6
+ export declare const CODEX_ADAPTER: ProviderLimitAdapter;
7
+ /** OpenCode Go's 5-hour, weekly and monthly subscription windows. */
8
+ export declare const OPENCODE_GO_ADAPTER: ProviderLimitAdapter;
9
+ /** The documented adapter for a provider, when one exists. Headers remain the other source. */
10
+ export declare function adapterFor(provider: string): ProviderLimitAdapter | null;
11
+ export declare const DOCUMENTED_ADAPTERS: readonly ProviderLimitAdapter[];
@@ -0,0 +1,349 @@
1
+ import { authHeaders, failure, getJson } from "./http.js";
2
+ import { toIso } from "./normalize.js";
3
+ const providerEntry = (provider, observedAt, limit) => ({ ...limit, provider, observedAt, source: 'provider' });
4
+ const percent = (used, limit) => (limit > 0 ? Math.min(100, Math.max(0, (used / limit) * 100)) : null);
5
+ export const OPENROUTER_ADAPTER = {
6
+ id: 'openrouter-key',
7
+ providers: ['openrouter'],
8
+ stability: 'documented',
9
+ async read({ provider, auth, baseUrl, fetch, signal, now }) {
10
+ const base = (baseUrl || 'https://openrouter.ai/api/v1').replace(/\/$/, '');
11
+ const reading = await getJson(fetch, `${base}/key`, authHeaders(auth), signal);
12
+ if (reading.status < 200 || reading.status >= 300)
13
+ throw new Error(failure(reading.status, 'OpenRouter'));
14
+ const data = reading.body?.data;
15
+ if (!data || typeof data !== 'object')
16
+ return { limits: [], reason: 'OpenRouter returned no key details.' };
17
+ const observedAt = now.toISOString();
18
+ const limits = [];
19
+ const limit = typeof data.limit === 'number' ? data.limit : null;
20
+ const remaining = typeof data.limit_remaining === 'number' ? data.limit_remaining : null;
21
+ if (limit !== null && remaining !== null) {
22
+ const used = Math.max(0, limit - remaining);
23
+ limits.push(providerEntry(provider, observedAt, {
24
+ id: `${provider}:key-credits`,
25
+ label: 'Key credits',
26
+ kind: 'credit',
27
+ unit: 'usd',
28
+ used,
29
+ limit,
30
+ remaining,
31
+ usedPercent: percent(used, limit),
32
+ remainingPercent: percent(remaining, limit),
33
+ status: remaining <= 0 ? 'rejected' : percent(used, limit) >= 80 ? 'warning' : 'allowed'
34
+ }));
35
+ }
36
+ const budgets = Array.isArray(data.budgets) ? data.budgets : [];
37
+ for (const value of budgets.slice(0, 8)) {
38
+ if (!value || typeof value !== 'object')
39
+ continue;
40
+ const budget = value;
41
+ const cap = typeof budget.limit_usd === 'number' ? budget.limit_usd : null;
42
+ const spend = typeof budget.spend_usd === 'number' ? budget.spend_usd : null;
43
+ if (cap === null || spend === null)
44
+ continue;
45
+ const id = typeof budget.budget_id === 'string' ? budget.budget_id : typeof budget.guardrail_id === 'string' ? budget.guardrail_id : `${budget.scope}`;
46
+ const interval = typeof budget.reset_interval === 'string' ? budget.reset_interval : 'lifetime';
47
+ limits.push(providerEntry(provider, observedAt, {
48
+ id: `${provider}:budget-${id}`,
49
+ label: `Budget (${interval})`,
50
+ kind: 'credit',
51
+ unit: 'usd',
52
+ used: spend,
53
+ limit: cap,
54
+ remaining: typeof budget.remaining_usd === 'number' ? budget.remaining_usd : Math.max(0, cap - spend),
55
+ usedPercent: percent(spend, cap),
56
+ remainingPercent: percent(typeof budget.remaining_usd === 'number' ? budget.remaining_usd : Math.max(0, cap - spend), cap),
57
+ resetsAt: toIso(typeof budget.resets_at === 'string' ? budget.resets_at : undefined, now),
58
+ status: spend >= cap ? 'rejected' : percent(spend, cap) >= 80 ? 'warning' : 'allowed'
59
+ }));
60
+ }
61
+ const free = data.free_model_daily_requests;
62
+ if (free && typeof free === 'object') {
63
+ const used = typeof free.used === 'number' ? free.used : null;
64
+ const cap = typeof free.limit === 'number' ? free.limit : null;
65
+ if (used !== null && cap !== null) {
66
+ limits.push(providerEntry(provider, observedAt, {
67
+ id: `${provider}:free-daily`,
68
+ label: 'Free-model requests today',
69
+ kind: 'rate',
70
+ unit: 'requests',
71
+ used,
72
+ limit: cap,
73
+ remaining: Math.max(0, cap - used),
74
+ usedPercent: percent(used, cap),
75
+ remainingPercent: percent(cap - used, cap),
76
+ status: used >= cap ? 'rejected' : percent(used, cap) >= 80 ? 'warning' : 'allowed'
77
+ }));
78
+ }
79
+ }
80
+ return {
81
+ limits,
82
+ plan: data.is_free_tier === true ? 'Free tier' : null,
83
+ reason: limits.length ? null : 'OpenRouter reports no credit limit for this key.'
84
+ };
85
+ }
86
+ };
87
+ export const DEEPSEEK_ADAPTER = {
88
+ id: 'deepseek-balance',
89
+ providers: ['deepseek'],
90
+ stability: 'documented',
91
+ async read({ provider, auth, baseUrl, fetch, signal, now }) {
92
+ const base = (baseUrl || 'https://api.deepseek.com').replace(/\/$/, '');
93
+ const reading = await getJson(fetch, `${base}/user/balance`, authHeaders(auth), signal);
94
+ if (reading.status < 200 || reading.status >= 300)
95
+ throw new Error(failure(reading.status, 'DeepSeek'));
96
+ const body = reading.body;
97
+ const available = body?.is_available === true;
98
+ const infos = Array.isArray(body?.balance_infos) ? body.balance_infos : [];
99
+ const observedAt = now.toISOString();
100
+ const limits = infos.slice(0, 4).flatMap((info) => {
101
+ if (!info || typeof info !== 'object')
102
+ return [];
103
+ const currency = typeof info.currency === 'string' ? info.currency : 'USD';
104
+ const total = typeof info.total_balance === 'string' ? Number(info.total_balance) : typeof info.total_balance === 'number' ? info.total_balance : null;
105
+ if (total === null || !Number.isFinite(total))
106
+ return [];
107
+ return [providerEntry(provider, observedAt, {
108
+ id: `${provider}:balance-${currency}`,
109
+ label: `Balance (${currency})`,
110
+ kind: 'credit',
111
+ unit: 'currency',
112
+ remaining: total,
113
+ detail: currency,
114
+ status: available ? (total > 0 ? 'allowed' : 'rejected') : 'rejected'
115
+ })];
116
+ });
117
+ return { limits, reason: limits.length ? null : 'DeepSeek returned no balance.' };
118
+ }
119
+ };
120
+ export const ANTHROPIC_ADMIN_ADAPTER = {
121
+ id: 'anthropic-rate-limits',
122
+ providers: ['anthropic'],
123
+ stability: 'documented',
124
+ async read({ provider, credentialType, auth, baseUrl, fetch, signal, now }) {
125
+ // A subscription sign-in is not an organization administrator.
126
+ if (credentialType !== 'api_key')
127
+ return { limits: [], reason: 'Organization limits need an Anthropic admin API key.' };
128
+ const base = (baseUrl || 'https://api.anthropic.com').replace(/\/$/, '');
129
+ const headers = { ...authHeaders(auth), 'anthropic-version': '2023-06-01' };
130
+ const reading = await getJson(fetch, `${base}/v1/organizations/rate_limits`, headers, signal);
131
+ if (reading.status < 200 || reading.status >= 300)
132
+ throw new Error(failure(reading.status, 'Anthropic'));
133
+ const data = reading.body?.data;
134
+ if (!Array.isArray(data))
135
+ return { limits: [], reason: 'Anthropic returned no rate limits for this credential.' };
136
+ const observedAt = now.toISOString();
137
+ const limits = [];
138
+ for (const value of data.slice(0, 24)) {
139
+ if (!value || typeof value !== 'object')
140
+ continue;
141
+ const group = value.group;
142
+ const groupName = typeof group?.display_name === 'string' ? group.display_name : typeof group?.type === 'string' ? group.type : 'Organization';
143
+ const groupId = typeof group?.id === 'string' ? group.id : groupName;
144
+ const entries = Array.isArray(value.limits) ? value.limits : [];
145
+ for (const item of entries) {
146
+ const type = typeof item.type === 'string' ? item.type : '';
147
+ const cap = typeof item.value === 'number' ? item.value : null;
148
+ if (!type || cap === null)
149
+ continue;
150
+ limits.push(providerEntry(provider, observedAt, {
151
+ id: `${provider}:configured-${groupId}-${type}`,
152
+ label: `${groupName} · ${type.replaceAll('_', ' ')}`,
153
+ kind: 'configured',
154
+ unit: type.includes('token') ? 'tokens' : 'requests',
155
+ limit: cap,
156
+ windowMinutes: 1,
157
+ status: 'configured',
158
+ detail: 'Configured limit, not current usage'
159
+ }));
160
+ }
161
+ }
162
+ return { limits, reason: limits.length ? null : 'Anthropic returned no configured rate limits.' };
163
+ }
164
+ };
165
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;
166
+ const pick = (value, keys) => {
167
+ if (!value)
168
+ return undefined;
169
+ for (const key of keys)
170
+ if (value[key] !== undefined && value[key] !== null)
171
+ return value[key];
172
+ return undefined;
173
+ };
174
+ const numeric = (value) => {
175
+ if (typeof value === 'number' && Number.isFinite(value))
176
+ return value;
177
+ if (typeof value === 'string' && value.trim()) {
178
+ const parsed = Number(value.trim());
179
+ return Number.isFinite(parsed) ? parsed : null;
180
+ }
181
+ return null;
182
+ };
183
+ const clampPercent = (value) => value === null ? null : Math.min(100, Math.max(0, value));
184
+ const windowLabel = (seconds) => {
185
+ if (seconds === null || seconds <= 0)
186
+ return 'Usage window';
187
+ const minutes = Math.round(seconds / 60);
188
+ if (minutes === 300)
189
+ return '5-hour window';
190
+ if (minutes === 10_080)
191
+ return 'Weekly window';
192
+ if (minutes >= 1_440)
193
+ return `${Math.round(minutes / 1_440)}-day window`;
194
+ if (minutes >= 60)
195
+ return `${Math.round(minutes / 60)}-hour window`;
196
+ return `${minutes} min window`;
197
+ };
198
+ const epochIso = (value) => {
199
+ if (value === null)
200
+ return null;
201
+ // The Codex usage endpoint reports seconds; tolerate milliseconds too.
202
+ const millis = value > 10_000_000_000 ? value : value * 1_000;
203
+ const date = new Date(millis);
204
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
205
+ };
206
+ /** The account id Pi's Codex OAuth credential carries in its access-token claims. */
207
+ function codexAccountId(token) {
208
+ if (!token)
209
+ return null;
210
+ const parts = token.split('.');
211
+ if (parts.length !== 3)
212
+ return null;
213
+ try {
214
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
215
+ const auth = record(payload['https://api.openai.com/auth']);
216
+ const id = auth?.chatgpt_account_id;
217
+ return typeof id === 'string' && id ? id : null;
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ }
223
+ /** ChatGPT/Codex plan windows and credits, from the account's own usage endpoint. */
224
+ export const CODEX_ADAPTER = {
225
+ id: 'codex-usage',
226
+ providers: ['openai-codex'],
227
+ stability: 'documented',
228
+ async read({ provider, auth, baseUrl, fetch, signal, now }) {
229
+ const base = (baseUrl || 'https://chatgpt.com/backend-api').replace(/\/$/, '');
230
+ const headers = { ...authHeaders(auth), accept: 'application/json' };
231
+ const accountId = codexAccountId(auth.apiKey);
232
+ if (accountId)
233
+ headers['chatgpt-account-id'] = accountId;
234
+ const reading = await getJson(fetch, `${base}/wham/usage`, headers, signal);
235
+ if (reading.status === 401 || reading.status === 403)
236
+ throw new Error('The ChatGPT sign-in cannot read Codex usage. Sign in again in Accounts.');
237
+ if (reading.status < 200 || reading.status >= 300)
238
+ throw new Error(failure(reading.status, 'Codex'));
239
+ const root = record(reading.body) ?? {};
240
+ const rate = record(pick(root, ['ratelimit', 'rateLimit', 'rate_limit']));
241
+ const observedAt = now.toISOString();
242
+ const limits = [];
243
+ for (const [name, keys] of [
244
+ ['primary', ['primarywindow', 'primaryWindow', 'primary_window']],
245
+ ['secondary', ['secondarywindow', 'secondaryWindow', 'secondary_window']]
246
+ ]) {
247
+ const window = record(pick(rate, keys));
248
+ const used = numeric(pick(window, ['usedpercent', 'usedPercent', 'used_percent']));
249
+ const reset = numeric(pick(window, ['resetat', 'resetAt', 'reset_at']));
250
+ const windowSeconds = numeric(pick(window, ['limitwindowseconds', 'limitWindowSeconds', 'limit_window_seconds']));
251
+ if (used === null && reset === null && windowSeconds === null)
252
+ continue;
253
+ const usedPercent = clampPercent(used);
254
+ limits.push(providerEntry(provider, observedAt, {
255
+ id: `${provider}:window-${name}`,
256
+ label: windowLabel(windowSeconds),
257
+ kind: 'window',
258
+ unit: 'percent',
259
+ usedPercent,
260
+ remainingPercent: usedPercent === null ? null : 100 - usedPercent,
261
+ windowMinutes: windowSeconds === null ? null : Math.round(windowSeconds / 60),
262
+ resetsAt: epochIso(reset),
263
+ status: usedPercent === null ? 'allowed' : usedPercent >= 100 ? 'rejected' : usedPercent >= 80 ? 'warning' : 'allowed'
264
+ }));
265
+ }
266
+ const credits = record(pick(root, ['credits']));
267
+ const balance = numeric(pick(credits, ['balance']));
268
+ const unlimited = pick(credits, ['unlimited']) === true;
269
+ const hasCredits = pick(credits, ['hascredits', 'hasCredits', 'has_credits']);
270
+ // Credits are an optional extra on ChatGPT plans: an empty balance is not a used-up plan window.
271
+ const includeCredits = unlimited || (balance !== null && (hasCredits === true || balance > 0));
272
+ if (includeCredits) {
273
+ limits.push(providerEntry(provider, observedAt, {
274
+ id: `${provider}:credits`,
275
+ label: 'Credits',
276
+ kind: 'credit',
277
+ unit: 'count',
278
+ remaining: unlimited ? null : balance,
279
+ status: unlimited ? 'allowed' : balance !== null && balance > 0 ? 'allowed' : 'warning',
280
+ detail: unlimited ? 'Unlimited credits' : 'ChatGPT credits'
281
+ }));
282
+ }
283
+ const plan = pick(root, ['plantype', 'planType', 'plan_type']);
284
+ return {
285
+ limits,
286
+ plan: typeof plan === 'string' && plan ? `ChatGPT ${plan}` : null,
287
+ reason: limits.length ? null : 'The ChatGPT account reported no Codex windows.'
288
+ };
289
+ }
290
+ };
291
+ /** OpenCode Go's 5-hour, weekly and monthly subscription windows. */
292
+ export const OPENCODE_GO_ADAPTER = {
293
+ id: 'opencode-go-usage',
294
+ providers: ['opencode-go'],
295
+ stability: 'documented',
296
+ async read({ provider, auth, fetch, signal, now }) {
297
+ const reading = await getJson(fetch, 'https://opencode.ai/zen/go/v1/usage', { ...authHeaders(auth), accept: 'application/json' }, signal);
298
+ if (reading.status === 401 || reading.status === 403)
299
+ throw new Error('The OpenCode API key cannot read Go usage. Check the key in Accounts.');
300
+ if (reading.status < 200 || reading.status >= 300)
301
+ throw new Error(failure(reading.status, 'OpenCode Go'));
302
+ const root = record(reading.body) ?? {};
303
+ const usage = record(pick(root, ['usage']));
304
+ if (!usage)
305
+ return { limits: [], reason: 'OpenCode Go returned no usage windows.' };
306
+ const renewsAt = typeof pick(usage, ['renewsAt', 'renews_at', 'endsAt', 'ends_at']) === 'string'
307
+ ? toIso(String(pick(usage, ['renewsAt', 'renews_at', 'endsAt', 'ends_at'])), now)
308
+ : typeof pick(root, ['renewsAt', 'renews_at']) === 'string'
309
+ ? toIso(String(pick(root, ['renewsAt', 'renews_at'])), now)
310
+ : null;
311
+ const observedAt = now.toISOString();
312
+ const limits = [];
313
+ const windows = [
314
+ { key: 'rolling', label: '5-hour window', minutes: 300 },
315
+ { key: 'weekly', label: 'Weekly window', minutes: 10_080 },
316
+ { key: 'monthly', label: 'Monthly window', minutes: 43_200 }
317
+ ];
318
+ for (const window of windows) {
319
+ const value = record(pick(usage, [window.key, `${window.key}Usage`]));
320
+ const usedPercent = clampPercent(numeric(pick(value, ['percent', 'usagePercent', 'usage_percent'])));
321
+ if (usedPercent === null)
322
+ continue;
323
+ const resetInSec = numeric(pick(value, ['resetInSec', 'reset_in_sec', 'resetInSeconds', 'resetsInSec']));
324
+ const resetAt = pick(value, ['resetAt', 'reset_at', 'resetsAt', 'resets_at', 'resetTime', 'reset_time']);
325
+ const resetsAt = resetInSec !== null
326
+ ? new Date(now.getTime() + Math.max(0, resetInSec) * 1_000).toISOString()
327
+ : typeof resetAt === 'string' ? toIso(resetAt, now) : window.key === 'monthly' ? renewsAt : null;
328
+ limits.push(providerEntry(provider, observedAt, {
329
+ id: `${provider}:window-${window.key}`,
330
+ label: window.label,
331
+ kind: 'window',
332
+ unit: 'percent',
333
+ usedPercent,
334
+ remainingPercent: 100 - usedPercent,
335
+ windowMinutes: window.minutes,
336
+ resetsAt,
337
+ status: usedPercent >= 100 ? 'rejected' : usedPercent >= 80 ? 'warning' : 'allowed',
338
+ detail: window.key === 'monthly' ? 'Monthly allowance' : null
339
+ }));
340
+ }
341
+ return { limits, reason: limits.length ? null : 'OpenCode Go returned no usage windows.' };
342
+ }
343
+ };
344
+ const ADAPTERS = [OPENROUTER_ADAPTER, DEEPSEEK_ADAPTER, ANTHROPIC_ADMIN_ADAPTER, CODEX_ADAPTER, OPENCODE_GO_ADAPTER];
345
+ /** The documented adapter for a provider, when one exists. Headers remain the other source. */
346
+ export function adapterFor(provider) {
347
+ return ADAPTERS.find((adapter) => adapter.providers.includes(provider)) ?? null;
348
+ }
349
+ export const DOCUMENTED_ADAPTERS = ADAPTERS;
@@ -0,0 +1,8 @@
1
+ import type { ProviderResponseLike } from "./types.js";
2
+ type Observe = (provider: string, response: ProviderResponseLike) => void;
3
+ /**
4
+ * Wraps every provider once so responses reach `observe`. Repeated calls do nothing, and a runtime
5
+ * that cannot be wrapped is used unchanged.
6
+ */
7
+ export declare function observeProviderResponses(models: unknown, observe: Observe): void;
8
+ export {};
@@ -0,0 +1,47 @@
1
+ const WRAPPED = Symbol('xingular-usage-observed');
2
+ function wrap(provider, method, observe) {
3
+ if (typeof method !== 'function')
4
+ return method;
5
+ return function observed(model, context, options) {
6
+ const previous = options?.onResponse;
7
+ const next = {
8
+ ...(options ?? {}),
9
+ onResponse: async (response) => {
10
+ try {
11
+ if (typeof previous === 'function')
12
+ await previous(response, model);
13
+ }
14
+ finally {
15
+ try {
16
+ observe(String(model?.provider ?? provider.id), response);
17
+ }
18
+ catch { /* observation is best effort */ }
19
+ }
20
+ }
21
+ };
22
+ return method.call(this, model, context, next);
23
+ };
24
+ }
25
+ /**
26
+ * Wraps every provider once so responses reach `observe`. Repeated calls do nothing, and a runtime
27
+ * that cannot be wrapped is used unchanged.
28
+ */
29
+ export function observeProviderResponses(models, observe) {
30
+ const target = models;
31
+ if (!target || typeof target.getProviders !== 'function' || typeof target.setProvider !== 'function')
32
+ return;
33
+ for (const provider of target.getProviders()) {
34
+ if (provider[WRAPPED])
35
+ continue;
36
+ try {
37
+ const clone = Object.create(Object.getPrototypeOf(provider), Object.getOwnPropertyDescriptors(provider));
38
+ clone.stream = wrap(provider, provider.stream, observe);
39
+ clone.streamSimple = wrap(provider, provider.streamSimple, observe);
40
+ clone[WRAPPED] = true;
41
+ target.setProvider(clone);
42
+ }
43
+ catch {
44
+ // An unusual provider is left as it is; limits simply stay unavailable for it.
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ import type { FetchLike } from "./types.js";
2
+ export interface JsonReading {
3
+ status: number;
4
+ body: unknown;
5
+ }
6
+ export declare function getJson(fetchImpl: FetchLike, url: string, headers: Record<string, string>, signal: AbortSignal): Promise<JsonReading>;
7
+ /** The headers a provider endpoint needs, from the credential Pi resolved. */
8
+ export declare function authHeaders(auth: {
9
+ apiKey?: string;
10
+ headers?: Record<string, string | null>;
11
+ }): Record<string, string>;
12
+ export declare function failure(status: number, provider: string): string;
@@ -0,0 +1,51 @@
1
+ // A small, bounded read of a provider's own usage endpoint. It sends only the account's resolved
2
+ // credential, never a body, never follows a redirect elsewhere, and never puts a credential in an
3
+ // error message.
4
+ import { scrub } from "../../accounts/store.js";
5
+ const MAX_BODY = 200_000;
6
+ const TIMEOUT_MS = 5_000;
7
+ function allowed(url) {
8
+ try {
9
+ const parsed = new URL(url);
10
+ return parsed.protocol === 'https:' || (parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'));
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export async function getJson(fetchImpl, url, headers, signal) {
17
+ if (!allowed(url))
18
+ throw new Error('The provider endpoint is not an HTTPS address.');
19
+ const timeout = AbortSignal.timeout(TIMEOUT_MS);
20
+ const combined = typeof AbortSignal.any === 'function' ? AbortSignal.any([signal, timeout]) : timeout;
21
+ const response = await fetchImpl(url, { headers, signal: combined, redirect: 'error' });
22
+ const text = (await response.text()).slice(0, MAX_BODY);
23
+ if (!text.trim())
24
+ return { status: response.status, body: null };
25
+ try {
26
+ return { status: response.status, body: JSON.parse(text) };
27
+ }
28
+ catch {
29
+ throw new Error(scrub(`The provider returned an unreadable response (${response.status}).`));
30
+ }
31
+ }
32
+ /** The headers a provider endpoint needs, from the credential Pi resolved. */
33
+ export function authHeaders(auth) {
34
+ const headers = {};
35
+ for (const [key, value] of Object.entries(auth.headers ?? {}))
36
+ if (typeof value === 'string' && value)
37
+ headers[key] = value;
38
+ if (auth.apiKey && !Object.keys(headers).some((key) => key.toLowerCase() === 'authorization' || key.toLowerCase() === 'x-api-key')) {
39
+ headers.authorization = `Bearer ${auth.apiKey}`;
40
+ }
41
+ return headers;
42
+ }
43
+ export function failure(status, provider) {
44
+ if (status === 401 || status === 403)
45
+ return `The ${provider} credential cannot read usage limits (a key with more access is required).`;
46
+ if (status === 404)
47
+ return `The ${provider} usage endpoint is not available at this address.`;
48
+ if (status === 429)
49
+ return `The ${provider} usage endpoint is rate limited. Try again later.`;
50
+ return `The ${provider} usage endpoint returned ${status}.`;
51
+ }
@@ -0,0 +1,15 @@
1
+ import type { Limit } from "../../accounts/store.js";
2
+ import type { ProviderResponseLike } from "./types.js";
3
+ export declare function toNumber(value: string | undefined): number | null;
4
+ /** A reset time as an ISO instant: epoch seconds or milliseconds, a duration, or a date string. */
5
+ export declare function toIso(value: string | undefined, now: Date): string | null;
6
+ export interface NormalizedHeaders {
7
+ limits: Limit[];
8
+ }
9
+ /**
10
+ * Reads every limit family present in one provider response. `provider` is the Pi provider id;
11
+ * the returned ids are namespaced with it so two providers never overwrite each other.
12
+ */
13
+ export declare function normalizeHeaders(provider: string, response: ProviderResponseLike, now?: Date): NormalizedHeaders;
14
+ /** Only the limit-bearing headers, bounded, so a child process never relays an entire response. */
15
+ export declare function limitHeaders(input: Record<string, string>): Record<string, string>;