pi-braid-providers 1.0.6 → 1.0.7

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.
package/README.md CHANGED
@@ -1,243 +1,94 @@
1
1
  # pi-braid-providers
2
2
 
3
- ```bash
4
- pi install npm:pi-braid-providers
5
- ```
3
+ Key rotation, provider failover, and route health for the [pi](https://github.com/earendil-works/pi)
4
+ coding agent: several keys per provider, a dead key retired instead of retried, and a failover to a
5
+ healthier route when a provider's ring runs dry.
6
6
 
7
- One command, no clone, no build: `pi install` registers the package with pi and installs it under pi's npm directory.
8
-
9
- Additional LLM providers for `pi-coding-agent`, built on a generalized **API key ring** for multi-key rotation and self-healing.
10
-
11
- The `providers` workspace registers providers into pi at session start. The key-ring machinery that both the agent loop and direct extension LLM calls use ships here; the provider itself is the **`agent-zero`** setup: the multi-vendor gateway that proxies Venice's models, configured from a catalog file resolved at runtime.
12
-
13
- > **A fresh install registers no provider.** The `agent-zero` model catalog
14
- > (Venice's models) and gateway URL are read from a userland catalog file
15
- > resolved at runtime (see `venice-catalog.ts`) that this package does not
16
- > include. Without it the extension loads with the key-ring machinery active
17
- > and zero registered providers; a synthetic route can be registered through
18
- > `registerRoute` (`ring-lifecycle.ts`), the seam the test suite uses for its
19
- > multi-route coverage.
20
-
21
- ## The key ring
22
-
23
- Each provider gets its own key ring with **exponential backoff**:
24
-
25
- - **Multi-key rotation.** Keys load from `${PREFIX}_P0` … `${PREFIX}_P9` in the
26
- environment, falling back to the bare `PREFIX` var when no numbered keys
27
- exist. On a quota/auth/403 error the active key is exhausted and the next
28
- request serves the next healthy key.
29
- - **Persisted state.** Exhausted-key state persists to
30
- `~/.pi/agent/.key-ring-state-<provider>.json`, so restarts skip known-dead
31
- keys instead of retrying them and hitting the same 403. The file lives in
32
- pi's agent directory (`state-paths.ts` owns the resolution); if no absolute
33
- home can be resolved, the ring refuses to persist and says so rather than
34
- writing a path relative to the cwd.
35
- - **Self-healing turn retry.** When a turn fails and a fresh key remains, an
36
- explicit marker is appended so pi-core re-runs the turn on the next key
37
- automatically. Fatal account-level errors (quota exhausted, billing, balance)
38
- are never masqueraded as retryable.
39
- - **Direct-call feedback.** Direct extension LLM calls (an extension's own
40
- `completeSimple`/`streamSimple` use)
41
- feed 429/403 back into the ring too, so no dead key is served on those paths.
42
- - **Diagnostics.** Rotation/exhaustion events route through a throttled notify
43
- sink (not `console.*`); a loud _all-keys-exhausted_ warning fires when every
44
- key is in backoff.
45
-
46
- ## Installation
7
+ ## Install
47
8
 
48
9
  ```bash
49
10
  pi install npm:pi-braid-providers
50
11
  ```
51
12
 
52
- Or register the workspace in `~/.pi/agent/settings.json`:
53
-
54
- ```json
55
- {
56
- "packages": ["file:~/pi-braid/packages/providers"]
57
- }
58
- ```
59
-
60
- ## Pre-send payload guard
13
+ Then name your routes (see Configuration). Until a route and its key are configured, the extension
14
+ loads the machinery and registers no provider: inert, not broken.
61
15
 
62
- | Setting | Default | Effect | Read cadence |
63
- | ------------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
64
- | `braid.context.presendGuard.enabled` | `true` | When enabled, if an estimated request exceeds 90% of a known model context window, oversized strings that can be flushed are written losslessly to disk and replaced by stubs. `false` skips the guard; unknown windows are not guessed. | Re-read at use time for every provider request. |
16
+ ## What you get
65
17
 
66
- ## What this does not do
18
+ | Surface | What it gives you |
19
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | Multi-key rotation | Numbered keys per provider, falling back to the bare variable. On a quota, auth or 403 error the active key is retired and the next request uses the next healthy one |
21
+ | Persisted state | Key-ring state on disk, so a restart skips keys already known dead instead of retrying the same 403 |
22
+ | Turn retry | When a turn fails and a fresh key remains, the next key serves the retry; fatal account errors are never disguised as retryable |
23
+ | Failover | An ordered route table, walked one rotation per episode, with one notice to the owner and a bounded fail-back probe that returns when the dead route recovers |
24
+ | Route health | `ring_health` reports each ring and each route's last probe: read-only, key hashes only, never writes |
25
+ | Request guard | `braid.context.presendGuard` flushes oversized strings to disk above 90% of a known window, so a large request shrinks instead of being rejected |
67
26
 
68
- - It does not ship a provider catalog. The `agent-zero` model list and gateway URL are read from a
69
- catalog file resolved at runtime; without it the extension loads the key-ring machinery and
70
- registers zero providers.
71
- - It does not rotate to a route you did not configure - a route is recognized only by the model ids
72
- it declares.
73
- - It does not retry a fatal account error (quota exhausted, billing, balance) as if it were transient.
27
+ ## Configuration
74
28
 
75
- ## Activation preconditions
29
+ ### Routes
76
30
 
77
- - `AGENT_ZERO_API_KEY` (or the numbered `AGENT_ZERO_API_KEY_P0`-`P9` rotation keys) set.
78
- - A userland catalog file for the `agent-zero` route; otherwise zero providers register and only a
79
- synthetic route (via `registerRoute`) can serve traffic.
31
+ `braid.providers.routes` is an ordered array; array order is rotation order. Each entry names the
32
+ provider id pi will expose, the environment variable prefix its keys load from, and optionally the
33
+ model ids it serves and a non-billable status probe:
80
34
 
81
- ## Environment variables
82
-
83
- ```bash
84
- AGENT_ZERO_API_KEY=<your-agent-zero-gateway-key>
85
- # Optional: multiple gateway keys for rotation
86
- AGENT_ZERO_API_KEY_P0=<key-1>
87
- AGENT_ZERO_API_KEY_P1=<key-2>
35
+ ```json
36
+ {
37
+ "braid": {
38
+ "providers": {
39
+ "routes": [
40
+ {
41
+ "provider": "venice-direct",
42
+ "keys": "VENICE_API_KEY",
43
+ "probe": {
44
+ "url": "https://api.venice.ai/api/v1/api_keys/rate_limits",
45
+ "balanceFloorUsd": 1
46
+ }
47
+ }
48
+ ]
49
+ }
50
+ }
51
+ }
88
52
  ```
89
53
 
90
- Env vars are resolved at request time by the key ring (`auth.apiKey.resolve`).
91
- `warnBelowKeyCount` (default 4) surfaces keys-remaining on each rotation so
92
- you know when to top up.
54
+ A route is recognized by the model ids it declares, so a route you did not configure is never
55
+ routed to. With no `routes` key, the two routes this build carries are used in order: the gateway
56
+ route, whose model catalog comes from a userland file that this package does not ship, and
57
+ `venice-direct`, which serves Venice's own API. Without that catalog the gateway route does not
58
+ register, and only routes you configure serve traffic. A route's `probe` declares a status
59
+ endpoint that is checked with the route's own key, at registration and on a bounded interval
60
+ afterwards; the check is never a billable call, and its result steers routing only while it is
61
+ fresher than `braid.providers.probeFreshnessMs`.
93
62
 
94
- ## The two routes
63
+ ### Settings
95
64
 
96
- When the userland catalog exists, two providers register:
65
+ | Key | Default | Effect |
66
+ | ------------------------------------ | --------------------- | --------------------------------------------------------------------------------- |
67
+ | `braid.providers.routes` | the pair above | Ordered route table; a configured array replaces the default pair entirely |
68
+ | `braid.providers.probeFreshnessMs` | `900000` (15 minutes) | How long a recorded probe or death may steer routing; `0` disables probe steering |
69
+ | `braid.context.presendGuard.enabled` | `true` | The 90% request guard described above |
97
70
 
98
- | Provider id | Serves | Via | Key prefix |
99
- | --------------- | ------------------ | --------------------------------------------------- | -------------------- |
100
- | `agent-zero` | Venice's models | agent-zero gateway (`https://llm.agent-zero.ai/v1`) | `AGENT_ZERO_API_KEY` |
101
- | `venice-direct` | The same model ids | Venice's own API (`https://api.venice.ai/api/v1`) | `VENICE_API_KEY` |
71
+ ### Environment
102
72
 
103
- `agent-zero` is the primary. On any registered route's key-ring death (a quota/auth/403 error that leaves its ring with zero usable keys), the **rotation engine** (ring-lifecycle.ts) walks the ordered route table and engages the first route that is not the dead route, has a usable key, and can serve the dead turn's model id - directly (a shared id) or through its `models` map. The order is `braid.providers.routes` (`[{ provider, keys, modelIds?, models?, venicePayload? }]`), array order being the rotation order; with no `routes` key the default pair is `agent-zero` then `venice-direct`, so an `agent-zero` death still falls over to Venice's own API. The table is the only attribution source: a route is recognized by the model ids it declares (`modelIds` plus the provider ids its `models` map resolves to) and takes the Venice payload tweak only when it sets `venicePayload` - off by default for a configured route, on for the default pair. A route the owner did not configure is neither routed to nor shaped. Exactly one rotation per episode: the engage swaps the session model for the retrying turn and emits one owner-visible whisper. A bounded fail-back probe (`FAILBACK_PROBE_INTERVAL_MS`, 10 min) re-checks the dead route at most once per interval at `turn_end` boundaries; when it recovers the session fails back automatically (one fail-back whisper) and the engage re-arms. State is in-memory per extension load; `session_start` resets the engine.
73
+ `AGENT_ZERO_API_KEY` for the gateway route, `VENICE_API_KEY` for the direct route; numbered
74
+ suffixes (`_P0` through `_P9`) add rotation keys, and the bare variable is used when no numbered
75
+ key exists.
104
76
 
105
- ## Health-routed entries (`auto`)
77
+ ## Limits
106
78
 
107
- A route can carry a virtual entry selectable as `<provider>/auto`: one model
108
- that answers each request with the first healthy route in the configured order,
109
- so a per-spawn route decision becomes a per-request one. The auto route spans
110
- several providers, so its entry lives under this package's own `braid`
111
- namespace: `braid/auto` exists whenever this build carries the agent-zero route;
112
- `opencode-go/auto` whenever the owner's route table names that provider -
113
- `opencode-go` sits ahead of the gateway in the live table, so a session that
114
- starts there leaves by the health walk instead of by hand. The entry declares
115
- no context window, max tokens or input types (pi reads those from the physical
116
- model that answered) and never dispatches its own id: a request with no
117
- previous physical model resolves its base id from the route table, or from the
118
- configured `defaultModel`, and the thinking level is resolved against the
119
- target model's own map. A provider that already ships a physical `auto` skips
120
- its own entry with one diagnostic, so a virtual entry can never shadow a real
121
- model.
79
+ - It is not a proxy or a gateway you can point other tools at: it plugs into pi's model registry.
80
+ - A route you did not configure is never routed to.
81
+ - Fatal account errors (quota, billing, balance) stop the turn rather than rotate.
82
+ - Ring and route state live directly under `~/.pi/agent/`, not under `braid/`:
83
+ `.key-ring-state-<provider>.json` and `.provider-route-state.json`. Key material is never
84
+ written, only key hashes.
122
85
 
123
- ## Route probes and cross-session state
124
-
125
- A route may declare a non-billable status capability in the ordered route table:
126
-
127
- ```json
128
- {
129
- "provider": "venice-direct",
130
- "keys": "VENICE_API_KEY",
131
- "probe": {
132
- "url": "https://api.venice.ai/api/v1/api_keys/rate_limits",
133
- "balanceFloorUsd": 1
134
- }
135
- }
136
- ```
86
+ ## For maintainers
137
87
 
138
- When present, one probe runs at extension registration - a plain GET at the
139
- declared URL with the route's own key as a bearer token, never an inference
140
- call - and the outcome is recorded per route in
141
- `~/.pi/agent/.provider-route-state.json`: the last probe, the last recorded
142
- death (its key identity as a hash, never the key), the fail-back target and the
143
- warmed model ids. A later session reads that file instead of re-learning it.
144
- The probe never throws, never rotates a key and never starts a billable call;
145
- the write is lock- and CAS-guarded exactly like the key-ring state, because
146
- every pi process on the host shares the file.
147
-
148
- A recorded outcome carries the endpoint's own remaining figure as
149
- `remainingUsd` (Venice's `balances.USD`, OpenRouter's `limit_remaining`), so no
150
- consumer has to derive remaining by subtracting a monthly figure from a cap. A
151
- route whose status document reports a spendable USD balance is classified
152
- against `probe.balanceFloorUsd` (default `1`, owner-configurable per route):
153
- above the floor is `ok`, at or below it but above zero is `degraded`, and zero
154
- or below is `unavailable`. A route whose status endpoint answers 200 while the
155
- account has no spendable balance therefore no longer reads as healthy.
156
-
157
- Two rules keep the layers honest:
158
-
159
- - **The runner is provider-agnostic.** Which provider ids exist and what their
160
- status responses look like lives in ONE adapter table in `probe.ts`. A
161
- provider absent from it is reported unavailable rather than guessed at.
162
- - **The state store interprets nothing.** It writes down an opaque record per
163
- route and reads it back; the probe vocabulary and the death classes belong to
164
- the layers that produce them.
165
-
166
- A route without a `probe` descriptor is never probed. Absent probes, resolution
167
- and rotation are unchanged.
168
-
169
- ## Ring and route health
170
-
171
- `ring-health.ts` is the read side of those two files: it summarizes them for a
172
- human or a consumer script without writing, locking or interpreting anything, so
173
- "which keys are in backoff, and what did each route last look like" needs no
174
- knowledge of either store's write protocol.
175
-
176
- - `ringHealth(providerId, { stateDir?, file? })` reads one ring file
177
- (`~/.pi/agent/.key-ring-state-<provider>.json`, or an explicit path).
178
- `{ provider, present: false }` means no file, or one this reader cannot
179
- trust; otherwise it carries `rev`, `savedAt`, `activeIndex` and one entry per
180
- key: `{ exhausted, exhaustedAt, retryCount, exhaustedBy, keyHash }`.
181
- - `routeHealth({ stateDir?, providers?, routeState? })` reports every route it
182
- can see - the ids the caller names, the routes the route store already
183
- carries, and the providers with a ring file - as
184
- `{ provider, ring, lastProbe, deathClass, deathAt, failback, warmModels }`.
185
- Facts the route store has not recorded come back as `null`, `0` or `[]`.
186
-
187
- Four rules hold the reader to its layer:
188
-
189
- - **Read-only.** No write, no lock, no migration. The stores own the one write
190
- protocol; a reader that could write would be a second one.
191
- - **Hash-only.** The ring file holds `hash`, never the key, and the reader
192
- copies named fields instead of forwarding the document - so a value the ring
193
- never wrote cannot leak through here.
194
- - **Provider-blind.** Every provider id is an argument or is discovered from
195
- the files; none is hardcoded, so a configured route table needs no code
196
- change here.
197
- - **Tolerant, never throwing.** An absent file is the normal empty state and
198
- stays silent; a malformed, partial or wrong-version file is the same
199
- `present: false` plus exactly one diagnostic.
200
-
201
- The reader takes its file paths from its caller or derives them from the
202
- provider id; it writes no state file of its own and reads the route document
203
- through `route-state.ts`'s reader. Nothing in `packages/core` imports it: an
204
- external consumer imports the package, never the other way round.
205
-
206
- `formatRingHealth(routes, freshness)` and `summarizeRouteHealth(routes, freshness)`
207
- turn a report into the `ring_health` tool's text and its `details` payload.
208
-
209
- - `braid.providers.probeFreshnessMs` (default `900000` ms, 15 minutes) controls
210
- how long a recorded unavailable probe or shape-death result can steer routing.
211
- `0` or less disables probe steering. The ring-health renderers receive the
212
- same resolved window and mark a death `(expired)` when the router's
213
- `deathInForce` predicate would no longer act on it, its timestamp is absent,
214
- or the window is off.
215
-
216
- ## Disabling the provider
217
-
218
- The registration lives in `index.ts` behind the venice-catalog seam: remove
219
- (or chmod) the userland catalog file and no provider is registered. All
220
- ring-lifecycle logic stays in `ring-lifecycle.ts`, unit-testable without
221
- loading pi-ai.
222
-
223
- ## Package layout
88
+ The route table internals, the probe adapter table, the state store protocol and the failure
89
+ classes are in [AGENTS.md](https://github.com/kifirkin/pi-braid/blob/main/packages/providers/AGENTS.md),
90
+ which the tarball does not carry.
224
91
 
225
- ```
226
- providers/
227
- ├── index.ts # registers the configured routes + wires key-ring/rotation events
228
- ├── venice-catalog.ts # userland catalog seam (the models file, resolved at runtime)
229
- ├── venice-direct.ts # the venice-direct route module: constants, key ring, model configs
230
- ├── key-ring.ts # multi-key rotation + backoff + persistence
231
- ├── ring-lifecycle.ts # per-request ring attribution, exhaust/self-heal, route-order rotation engine
232
- ├── probe.ts # provider-agnostic probe runner + the adapter table
233
- ├── route-state.ts # cross-session route state, lock + CAS writes
234
- ├── ring-health.ts # read-only health surface over the key-ring + route state files
235
- ├── log.ts # notify-sink routing (no console.*)
236
- └── test/ # fixture-driven behavior pins
237
- ```
92
+ ## License
238
93
 
239
- > The registered id was `venice`, then `a0`, now `agent-zero` - each rename
240
- > follows the gateway the provider serves models through. `venice-direct`, the
241
- > `VENICE_API_KEY` family and `venice-models.json` are unchanged; the gateway's
242
- > own credential namespace moved from `A0_VENICE_API_KEY` to `AGENT_ZERO_API_KEY`
243
- > with the last rename.
94
+ [MIT](LICENSE)
package/build-info.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "pi-braid-providers",
3
- "version": "1.0.6",
4
- "sha": "752d2e6e9b45037a8ebcc17f3d6f90c905b2c71c",
5
- "shaShort": "752d2e6",
3
+ "version": "1.0.7",
4
+ "sha": "3a3de965131cd125b58a807b064a43d37b7f466f",
5
+ "shaShort": "3a3de965",
6
6
  "branch": "detached",
7
7
  "dirty": false,
8
- "packedAt": "2026-10-19T11:29:20.919Z"
8
+ "packedAt": "2026-11-11T22:45:21.903Z"
9
9
  }
package/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { createProvider } from "@earendil-works/pi-ai";
3
- // `Type` ships with pi-ai's own typebox re-export, so the tool schema adds no
4
- // dependency to a package that resolves only @earendil-works/* from npm.
5
- import { Type } from "@earendil-works/pi-ai";
3
+ // pi-ai re-exported typebox's `Type` until 1.5.0 and no longer does, so the
4
+ // builder comes from the package that owns it, at the peer floor the sibling
5
+ // packages declare.
6
+ import { Type } from "typebox";
6
7
  import {
7
8
  completeSimple,
8
9
  openAICompletionsApi,
@@ -48,7 +49,11 @@ import {
48
49
  import { loadVeniceCatalog } from "./venice-catalog.js";
49
50
  import type { ChatModelConfig } from "./venice-catalog.js";
50
51
  import { keyRingAuth, withKeyRingAuth } from "./ring-provider";
51
- import { shrinkDoomedPayload, isPresendGuardEnabled } from "./presend-guard";
52
+ import {
53
+ buildContextWindowIndex,
54
+ readPresendPolicy,
55
+ shrinkDoomedPayload,
56
+ } from "./presend-guard";
52
57
  import { probeRoute } from "./probe.js";
53
58
  import {
54
59
  formatRingHealth,
@@ -850,25 +855,29 @@ export default async function (pi: ExtensionAPI) {
850
855
  }
851
856
 
852
857
  // Model-id → catalog context window for the pre-send guard.
853
- // Built once at factory load from the catalog this package owns (venice-catalog.ts).
854
- const presendContextWindows = new Map<string, number>();
855
- for (const m of veniceCatalog?.models ?? []) {
856
- if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
857
- presendContextWindows.set(m.id, m.contextWindow);
858
- }
859
- }
858
+ // Built once at factory load from the catalog this package owns
859
+ // (venice-catalog.ts): exact ids, plus the normalized spelling a route may
860
+ // use for the same model. A dot-form request id (the route sends
861
+ // `deepseek-v4.1-flash`, the catalog spells `deepseek-v4-1-flash`) would
862
+ // otherwise resolve to no window and the guard would silently no-op.
863
+ const presendWindows = buildContextWindowIndex(veniceCatalog?.models ?? []);
860
864
 
861
865
  pi.on("before_provider_request", async (event: any) => {
862
866
  // Ring-lifecycle first (WAF/key shaping), then the pre-send guard on
863
- // the (possibly reshaped) payload. The guard no-ops unless the payload is
864
- // DOOMED for a KNOWN model; its returned payload (when present) replaces
865
- // the outgoing body per pi-core's emitBeforeProviderRequest semantics.
867
+ // the (possibly reshaped) payload. The token arm no-ops unless the payload
868
+ // is DOOMED for a KNOWN model; the byte arm no-ops unless the owner
869
+ // declared a cap (a request above the report threshold still writes one
870
+ // line). Its returned payload (when present) replaces the outgoing body
871
+ // per pi-core's emitBeforeProviderRequest semantics.
866
872
  const shaped = handleBeforeProviderRequest(event.payload);
867
873
  const outgoing = shaped ?? event.payload;
868
874
  try {
869
- if (!isPresendGuardEnabled()) return shaped;
875
+ const policy = readPresendPolicy();
876
+ if (!policy.enabled) return shaped;
870
877
  const guard = shrinkDoomedPayload(outgoing, {
871
- resolveContextWindow: (modelId) => presendContextWindows.get(modelId),
878
+ resolveContextWindow: (modelId) => presendWindows.resolve(modelId),
879
+ maxRequestBytes: policy.maxRequestBytes,
880
+ reportBytesAbove: policy.reportBytesAbove,
872
881
  });
873
882
  return guard?.payload ?? shaped;
874
883
  } catch {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-braid-providers",
3
- "version": "1.0.6",
4
- "description": "LLM provider for pi-coding-agent: Venice models via the agent-zero gateway, with a multi-key ring",
3
+ "version": "1.0.7",
4
+ "description": "Key rotation, provider failover, and route health for the pi coding agent, so one dead key or provider does not end the session.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "*.ts",
@@ -14,12 +14,12 @@
14
14
  },
15
15
  "keywords": [
16
16
  "pi-package",
17
- "pi",
17
+ "braid",
18
18
  "pi-coding-agent",
19
- "extension",
20
- "providers",
21
- "llm",
22
- "venice.ai"
19
+ "llm-provider",
20
+ "api-key-rotation",
21
+ "failover",
22
+ "venice"
23
23
  ],
24
24
  "pi": {
25
25
  "extensions": [
@@ -38,11 +38,12 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "6.0.2",
41
- "@earendil-works/pi-ai": "^1.2.0"
41
+ "@earendil-works/pi-ai": "^1.5.0"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-ai": "^1.2.0",
45
- "@earendil-works/pi-coding-agent": "^1.2.0"
44
+ "@earendil-works/pi-ai": "^1.5.0",
45
+ "@earendil-works/pi-coding-agent": "^1.5.0",
46
+ "typebox": "^1.1.38"
46
47
  },
47
48
  "author": "kifirkin",
48
49
  "license": "MIT",
@@ -56,6 +57,6 @@
56
57
  },
57
58
  "homepage": "https://github.com/kifirkin/pi-braid/tree/main/packages/providers",
58
59
  "engines": {
59
- "node": ">=22.19.0"
60
+ "node": ">=24.0.0"
60
61
  }
61
62
  }