@homeflare/alchemy 0.41.0 → 0.43.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.
- package/dist/litellm/budget-errors.d.ts +36 -0
- package/dist/litellm/budget-errors.d.ts.map +1 -0
- package/dist/litellm/budget-form.d.ts +50 -0
- package/dist/litellm/budget-form.d.ts.map +1 -0
- package/dist/litellm/budget-operations.d.ts +20 -0
- package/dist/litellm/budget-operations.d.ts.map +1 -0
- package/dist/litellm/budget.d.ts +61 -0
- package/dist/litellm/budget.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +5 -0
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/index.d.ts +2 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +125 -2
- package/dist/litellm/index.js.map +9 -5
- package/dist/litellm/operations.d.ts +22 -0
- package/dist/litellm/operations.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +1 -1
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role-wire.d.ts.map +1 -1
- package/dist/openbao/fake-engines-roles.d.ts.map +1 -1
- package/dist/openbao/index.js +9 -6
- package/dist/openbao/index.js.map +4 -4
- package/dist/postgres/connection.d.ts +20 -2
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/database-sql.d.ts +1 -1
- package/dist/postgres/database-sql.d.ts.map +1 -1
- package/dist/postgres/database.d.ts +1 -1
- package/dist/postgres/database.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +5 -3
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +124 -38
- package/dist/postgres/index.js.map +8 -7
- package/dist/postgres/providers.d.ts +4 -2
- package/dist/postgres/providers.d.ts.map +1 -1
- package/dist/postgres/psql-executor.d.ts +21 -0
- package/dist/postgres/psql-executor.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag-types.d.ts +57 -0
- package/dist/proxmox/ceph-flag-types.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag-wire.d.ts +77 -0
- package/dist/proxmox/ceph-flag-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +15 -50
- package/dist/proxmox/ceph-flag.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +10 -13
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/credential-errors.d.ts +18 -4
- package/dist/proxmox/credential-errors.d.ts.map +1 -1
- package/dist/proxmox/credentials.d.ts +35 -1
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/group.d.ts.map +1 -1
- package/dist/proxmox/index.js +805 -757
- package/dist/proxmox/index.js.map +21 -19
- package/dist/proxmox/lease-cache.d.ts +67 -5
- package/dist/proxmox/lease-cache.d.ts.map +1 -1
- package/dist/proxmox/mint.d.ts +4 -2
- package/dist/proxmox/mint.d.ts.map +1 -1
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/role.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/unreadable-read.d.ts +8 -1
- package/dist/proxmox/unreadable-read.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/credentials.md +14 -1
- package/docs/litellm-budget.md +55 -0
- package/docs/litellm.md +7 -6
- package/docs/postgres.md +22 -0
- package/docs/proxmox-sdk-migration.md +45 -0
- package/package.json +3 -3
- package/src/litellm/budget-errors.ts +38 -0
- package/src/litellm/budget-form.ts +94 -0
- package/src/litellm/budget-operations.ts +51 -0
- package/src/litellm/budget.test.ts +105 -0
- package/src/litellm/budget.ts +127 -0
- package/src/litellm/fake-litellm.ts +50 -1
- package/src/litellm/index.ts +13 -0
- package/src/litellm/operations.ts +1 -1
- package/src/litellm/providers.ts +4 -1
- package/src/openbao/auth-role-form.test.ts +37 -0
- package/src/openbao/auth-role-form.ts +12 -1
- package/src/openbao/auth-role-wire.test.ts +1 -0
- package/src/openbao/auth-role-wire.ts +13 -1
- package/src/openbao/distilled-retry.test.ts +1 -0
- package/src/openbao/fake-engines-roles.ts +7 -1
- package/src/postgres/connection.ts +37 -3
- package/src/postgres/database-sql.ts +4 -3
- package/src/postgres/database.ts +3 -2
- package/src/postgres/index.ts +10 -3
- package/src/postgres/providers.ts +15 -2
- package/src/postgres/psql-executor.test.ts +106 -0
- package/src/postgres/psql-executor.ts +130 -0
- package/src/proxmox/acl.ts +7 -1
- package/src/proxmox/api-token.ts +7 -1
- package/src/proxmox/ceph-flag-types.ts +70 -0
- package/src/proxmox/ceph-flag-wire.test.ts +72 -0
- package/src/proxmox/ceph-flag-wire.ts +123 -0
- package/src/proxmox/ceph-flag.ts +77 -130
- package/src/proxmox/ceph-osd-write.ts +10 -13
- package/src/proxmox/credential-errors.ts +19 -5
- package/src/proxmox/credentials.test.ts +73 -1
- package/src/proxmox/credentials.ts +72 -1
- package/src/proxmox/group.ts +7 -1
- package/src/proxmox/lease-cache-mint.test.ts +70 -0
- package/src/proxmox/lease-cache.test.ts +83 -7
- package/src/proxmox/lease-cache.ts +74 -10
- package/src/proxmox/mint.ts +12 -5
- package/src/proxmox/node-network.ts +7 -1
- package/src/proxmox/role.ts +7 -1
- package/src/proxmox/storage.ts +7 -1
- package/src/proxmox/unreadable-read.ts +16 -4
- package/src/proxmox/user.ts +7 -1
- package/src/proxmox/zfs-pool.ts +7 -1
|
@@ -47,23 +47,49 @@ import * as Cache from 'effect/Cache';
|
|
|
47
47
|
import * as Duration from 'effect/Duration';
|
|
48
48
|
import * as Effect from 'effect/Effect';
|
|
49
49
|
import * as Exit from 'effect/Exit';
|
|
50
|
-
import { type ApiTarget, type PveCredential, type PveRole } from './credentials.ts';
|
|
50
|
+
import { type ApiTarget, type BaoEnvironment, type PveCredential, type PveRole } from './credentials.ts';
|
|
51
51
|
/**
|
|
52
52
|
* What a credential is cached under.
|
|
53
53
|
*
|
|
54
|
-
* ⛔ KEYED ON THE OPENBAO MOUNT AND
|
|
55
|
-
* cluster-wide — `user.cfg` is replicated — so node-b, node-c and
|
|
56
|
-
* `api` would mint three identical tokens when failover
|
|
54
|
+
* ⛔ KEYED ON THE OPENBAO MOUNT AND TIER, NOT ON A MEMBER HOSTNAME OR ON `role` (M3, red team on
|
|
55
|
+
* K-T1). A PVE API token is cluster-wide — `user.cfg` is replicated — so node-b, node-c and
|
|
56
|
+
* node-d share one credential. Keying on `api` would mint three identical tokens when failover
|
|
57
|
+
* rotates members. `role` USED to be part of this key too, but the minted credential depends
|
|
58
|
+
* only on `(mount, scheme, tier)` — `role` only ever fed `mintTier` to re-derive the tier the
|
|
59
|
+
* key already carries — so `roles: { read: 'x', provision: 'x' }` (one tier for both roles, the
|
|
60
|
+
* shape `docs/credentials.md` itself recommends) used to open TWO cache entries and mint TWO
|
|
61
|
+
* byte-for-byte interchangeable tokens for what is provably one credential. Dropping `role`
|
|
62
|
+
* collapses that back to one entry without changing the default case, where `tier === role`
|
|
63
|
+
* already.
|
|
57
64
|
* ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-c1`, `proxmox-c2` and `pbs-c1` are
|
|
58
65
|
* distinct OpenBao mounts with distinct policies; two estates never share a mount name.
|
|
59
66
|
* ★ A PLAIN OBJECT IS A SAFE KEY BECAUSE EFFECT COMPARES KEYS STRUCTURALLY — two separately built
|
|
60
67
|
* literals with the same fields are one entry, which the probe above measured.
|
|
68
|
+
*
|
|
69
|
+
* ⛔ `tier` IS THE KEY, NOT `role` (K-T1). `targetForKey` below has to RECONSTRUCT a target from
|
|
70
|
+
* the key alone (it does not keep the caller's original `ApiTarget` around), and a
|
|
71
|
+
* `PveTarget.roles` override lives on THAT original target, not on `role` — so without `tier`
|
|
72
|
+
* in the key, every `leased()` call for an overridden role would resolve its override once at
|
|
73
|
+
* the first `keyOf`, then reuse whatever `targetForKey` happens to reconstruct on a cache miss,
|
|
74
|
+
* and TWO DIFFERENT overrides sharing one `{mount, scheme}` (e.g. one consumer's default `read`
|
|
75
|
+
* tier and another's `roles: {read: 'x'}` override, same mount) would collide into ONE cache
|
|
76
|
+
* entry and hand each other's credential to the wrong caller. Resolving the tier at `keyOf` —
|
|
77
|
+
* where the real target is still in hand — and keying on it closes that: two different tiers
|
|
78
|
+
* are two different entries, always.
|
|
61
79
|
*/
|
|
62
80
|
export type LeaseKey = {
|
|
63
81
|
readonly mount: string;
|
|
64
|
-
readonly role: PveRole;
|
|
65
82
|
readonly scheme: ApiTarget['scheme'];
|
|
83
|
+
readonly tier: string;
|
|
66
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* ⚠️ EXPORTED FOR THE TEST (I1, red team on K-T1). This is the one line the whole PR exists
|
|
87
|
+
* for — `tier: mintTier(target, role)` is what makes an overridden `PveTarget.roles` reach the
|
|
88
|
+
* cached path at all. Nothing else in this file's test suite calls `keyOf` or `leased`
|
|
89
|
+
* directly, so without this export a regression here (e.g. reverting to `tier: role`) passed
|
|
90
|
+
* the whole suite silently.
|
|
91
|
+
*/
|
|
92
|
+
export declare const keyOf: (target: ApiTarget, role: PveRole) => LeaseKey;
|
|
67
93
|
/**
|
|
68
94
|
* How long a freshly minted credential may be handed out again.
|
|
69
95
|
*
|
|
@@ -87,6 +113,42 @@ export declare const timeToLive: (exit: Exit.Exit<PveCredential, unknown>) => Du
|
|
|
87
113
|
* ⚠️ EXPORTED FOR THE TEST, which hands it a counting mint instead of calling OpenBao.
|
|
88
114
|
*/
|
|
89
115
|
export declare const makeLeases: <E, R>(mintFor: (key: LeaseKey) => Effect.Effect<PveCredential, E, R>) => Effect.Effect<Cache.Cache<LeaseKey, PveCredential, E, R>, never, never>;
|
|
116
|
+
/**
|
|
117
|
+
* ⛔ CARRIES `key.tier` BACK IN AS A `roles` OVERRIDE (K-T1), SO `mint`'S OWN `mintTier` CALL
|
|
118
|
+
* REPRODUCES THE SAME TIER `keyOf` ALREADY RESOLVED. This target is synthetic — it is never the
|
|
119
|
+
* caller's real `PveTarget` (`members`/`api` are throwaway placeholders `mint` never reads) —
|
|
120
|
+
* and, since M3 dropped `role` from `LeaseKey`, it no longer even knows which semantic role
|
|
121
|
+
* produced `key.tier`. It doesn't need to: `roles: { read: key.tier }` plus always minting with
|
|
122
|
+
* the fixed role `'read'` (below) makes `mintTier(targetForKey(key), 'read') === key.tier`
|
|
123
|
+
* unconditionally, because `mintTier` only ever consults the ONE role it's called with. Which
|
|
124
|
+
* role the ORIGINAL caller asked for is irrelevant here — `keyOf` already folded that into
|
|
125
|
+
* `key.tier` before this function ever sees the key.
|
|
126
|
+
* ⚠️ THE PBS BRANCH MINTS WITH `key.tier` ITSELF AS THE ROLE, NOT `'read'`. A `PbsTarget` has no
|
|
127
|
+
* `roles` override (see `credentials.ts`'s ⛔ on `PbsTarget`), so `mintTier` for `scheme:'pbs'`
|
|
128
|
+
* always returns its `role` argument unchanged — meaning `key.tier` for a PBS key IS the literal
|
|
129
|
+
* role name (`'read'`/`'provision'`) `keyOf` recorded. Passing anything else here would ask
|
|
130
|
+
* OpenBao for the wrong tier.
|
|
131
|
+
* ⚠️ EXPORTED FOR THE TEST, which checks that round trip directly rather than only through the
|
|
132
|
+
* cache's externally visible behaviour.
|
|
133
|
+
*/
|
|
134
|
+
export declare const targetForKey: (key: LeaseKey) => ApiTarget;
|
|
135
|
+
/**
|
|
136
|
+
* ⚠️ EXPORTED FOR THE TEST (second red-team pass on K-T1). `targetForKey`'s own tests call
|
|
137
|
+
* `mintTier(targetForKey(key), 'read')` to check the round trip, which hand-reproduces this
|
|
138
|
+
* function's fixed choice of `'read'` as the pve mint role rather than exercising it — change
|
|
139
|
+
* that choice here and those tests keep passing while a live cache miss mints the wrong tier.
|
|
140
|
+
* Testing this function directly (against a fake OpenBao) pins the choice itself.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ `env` DEFAULTS TO `process.env`, SAME AS `mint` ITSELF (credentials.ts) — the cache's real
|
|
143
|
+
* caller below (`Effect.runSync(makeLeases(mintForKey))`) always calls this with one argument,
|
|
144
|
+
* so the default is what production runs on. The parameter exists only so the test can pass a
|
|
145
|
+
* fake OpenBao's address without touching the real environment.
|
|
146
|
+
*/
|
|
147
|
+
export declare const mintForKey: (key: LeaseKey, env?: BaoEnvironment) => Effect.Effect<{
|
|
148
|
+
leaseSeconds: number;
|
|
149
|
+
secret: string;
|
|
150
|
+
tokenId: string;
|
|
151
|
+
}, Error | import("./credential-errors.ts").PveCredentialDenied, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
90
152
|
/**
|
|
91
153
|
* A credential for `role` on `target`: a still-valid lease when one is kept, one shared mint when
|
|
92
154
|
* not.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lease-cache.d.ts","sourceRoot":"","sources":["../../src/proxmox/lease-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAC;AACpC,OAAO,
|
|
1
|
+
{"version":3,"file":"lease-cache.d.ts","sourceRoot":"","sources":["../../src/proxmox/lease-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,OAAO,EAEb,MAAM,kBAAkB,CAAC;AAgB1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,WAAY,SAAS,QAAQ,OAAO,KAAG,QAIvD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,SAAU,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,KAAG,QAAQ,CAAC,QAG3D,CAAC;AAEpB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC,4EACX,CAAC;AAErF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY,QAAS,QAAQ,KAAG,SAGoC,CAAC;AAElF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,QAAS,QAAQ,QAAO,cAAc;;;;sHACwB,CAAC;AAItF;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,WAAY,SAAS,QAAQ,OAAO,qJAA2C,CAAC"}
|
package/dist/proxmox/mint.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minting one PVE/PBS credential from OpenBao: `GET /v1/<mount>/creds/<
|
|
2
|
+
* Minting one PVE/PBS credential from OpenBao: `GET /v1/<mount>/creds/<tier>`, alive for five
|
|
3
3
|
* minutes. Split out of `credentials.ts` (2026-09-24) to keep that file's TYPES under the
|
|
4
4
|
* 250-line cap once `PveCredentialDenied`'s catchers needed a real header there; the reasoning
|
|
5
5
|
* for WHY the estate mints rather than stores a token is still credentials.ts's own header.
|
|
@@ -12,7 +12,9 @@ import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
|
12
12
|
import { PveCredentialDenied } from './credential-errors.ts';
|
|
13
13
|
import type { ApiTarget, BaoEnvironment, PveCredential, PveRole } from './credentials.ts';
|
|
14
14
|
/**
|
|
15
|
-
* Mint one credential for `role`: `GET /v1/<mount>/creds/<
|
|
15
|
+
* Mint one credential for `role`: `GET /v1/<mount>/creds/<tier>` on OpenBao's HTTP API, where
|
|
16
|
+
* `tier` is `mintTier(target, role)` (credentials.ts) — `role` itself unless `target.roles`
|
|
17
|
+
* overrides it (K-T1).
|
|
16
18
|
*
|
|
17
19
|
* 🔴 IT SHELLED OUT TO `bao read -format=json` UNTIL 2026-09-14. The case for the CLI was that one
|
|
18
20
|
* client could not disagree with `bao kv get` about what this machine may do. <estate>/openbao then
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mint.d.ts","sourceRoot":"","sources":["../../src/proxmox/mint.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"mint.d.ts","sourceRoot":"","sources":["../../src/proxmox/mint.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAM1F;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,IAAI,WAAY,SAAS,QAAQ,OAAO,QAAO,cAAc;;;;sDAwGtE,CAAC;AAEL;;;;;GAKG;AACH,eAAO,MAAM,aAAa,eAAgB,aAAa,UAAU,SAAS,KAAG,MAGf,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-network.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAGL,KAAK,gBAAgB,EAGtB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"node-network.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAGL,KAAK,gBAAgB,EAGtB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAG1D,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIrF,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAiCH,eAAO,MAAM,0BAA0B,wIA4FpC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/proxmox/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAa7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/proxmox/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAa7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUtE,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAwBjE,eAAO,MAAM,mBAAmB,iIAoF7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAatE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,gGAAgG;AAChG,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,qGAAqG;AACrG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAEH,eAAO,MAAM,sBAAsB,oIA4GhC,CAAC"}
|
|
@@ -45,6 +45,13 @@ export declare const readOrUnreadable: <A, E, R>(effect: Effect.Effect<A, E | Pv
|
|
|
45
45
|
* in `bun run plan`'s own output (Effect's default logger), even though the Diff type has
|
|
46
46
|
* nowhere to carry the distinction through to the printed action. See the header for why a log
|
|
47
47
|
* line, not a failed diff or a fourth Diff action, is what "the plan reports" means here.
|
|
48
|
+
*
|
|
49
|
+
* ⛔ M6 (Opus red team, K-T1): NAMES `mount` AND `tier`, NOT JUST THE RESOURCE. Before this, the
|
|
50
|
+
* line read "no grant for this credential's role" with nothing to act on — an operator debugging
|
|
51
|
+
* a scoped-down `PveTarget.roles` override (the whole point of K-T1) had to go re-derive which
|
|
52
|
+
* mount and tier the read actually asked OpenBao for before they could even open that mount's
|
|
53
|
+
* policy. `PveCredentialDenied`'s own message already made this exact call — reading `tier`, not
|
|
54
|
+
* `role`, per that class's own ⛔ — so this warning takes the same two fields for the same reason.
|
|
48
55
|
*/
|
|
49
|
-
export declare const unreadableWarning: (type: string, identity: string) => Effect.Effect<void>;
|
|
56
|
+
export declare const unreadableWarning: (type: string, identity: string, mount: string, tier: string) => Effect.Effect<void>;
|
|
50
57
|
//# sourceMappingURL=unreadable-read.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unreadable-read.d.ts","sourceRoot":"","sources":["../../src/proxmox/unreadable-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,sFAAsF;AACtF,eAAO,MAAM,UAAU,eAA2B,CAAC;AACnD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,mBAAmB,EAAE,CAAC,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE,CAAC,CACkD,CAAC;AAExF
|
|
1
|
+
{"version":3,"file":"unreadable-read.d.ts","sourceRoot":"","sources":["../../src/proxmox/unreadable-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,sFAAsF;AACtF,eAAO,MAAM,UAAU,eAA2B,CAAC;AACnD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,mBAAmB,EAAE,CAAC,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE,CAAC,CACkD,CAAC;AAExF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,SACtB,MAAM,YACF,MAAM,SACT,MAAM,QACP,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAKlB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAetE,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAEjE,eAAO,MAAM,mBAAmB,iIAqG7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAItE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,mGAAmG;AACnG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAEH,eAAO,MAAM,sBAAsB,oIA8EhC,CAAC"}
|
package/docs/credentials.md
CHANGED
|
@@ -12,7 +12,20 @@ a denied grant rather than a missing file, and `Bearer ` + nothing 401s in a way
|
|
|
12
12
|
like a bad credential.
|
|
13
13
|
|
|
14
14
|
The Proxmox subpath mints every call's token from an OpenBao mount instead; see
|
|
15
|
-
[proxmox.md](./proxmox.md).
|
|
15
|
+
[proxmox.md](./proxmox.md). Every call still asks for one of two semantic roles, `read` or
|
|
16
|
+
`provision` (`PveRole`, `credentials.ts`) — a `PveTarget` can optionally override which OpenBao
|
|
17
|
+
mint TIER NAME each one actually resolves to (`roles: { read?, provision? }`, K-T1, 2026-09-27):
|
|
18
|
+
one consumer with its own least-privilege tier for both roles on the SAME mount sets
|
|
19
|
+
`roles: { read: 'my-tier', provision: 'my-tier' }` and every call this package already makes
|
|
20
|
+
mints that tier instead, with no new call site anywhere in the package. `credentials.ts`'s
|
|
21
|
+
`mintTier` is the one place that resolution happens; `mint.ts` and `lease-cache.ts` both call it
|
|
22
|
+
rather than reading `role`/`target.roles` directly — see `mintTier`'s own header for why the
|
|
23
|
+
lease cache specifically needed it too, not just the mint call.
|
|
24
|
+
|
|
25
|
+
⛔ **Override both roles or neither.** An unmentioned role is not scoped down — it still mints
|
|
26
|
+
the estate-wide `read`/`provision` tier. Setting only `roles: { read: 'my-tier' }` leaves every
|
|
27
|
+
reconcile/delete on that target minting the full-privilege `provision` credential while the plan
|
|
28
|
+
output looks entirely scoped down.
|
|
16
29
|
|
|
17
30
|
The Forgejo subpath (2026-09-23, moved onto `@distilled.cloud/forgejo`) resolves credentials
|
|
18
31
|
through the package's own `CredentialsFromEnv` layer, which reads `FORGEJO_URL` / `FORGEJO_TOKEN`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# LiteLLM — `LiteLLM.Budget`
|
|
2
|
+
|
|
3
|
+
One row of LiteLLM's `LiteLLM_BudgetTable` (`/budget/*`): the "tier" that virtual keys and teams
|
|
4
|
+
bind to (`hf-tier-agent`, `hf-tier-interactive`, …). Typed calls come from
|
|
5
|
+
`@distilled.cloud/litellm/budget_management`; credentials and refusals are the family's, see
|
|
6
|
+
[litellm.md](./litellm.md).
|
|
7
|
+
|
|
8
|
+
## Monitoring, not limits
|
|
9
|
+
|
|
10
|
+
`max_budget` is a **hard cap**: LiteLLM's own field doc says "Requests will fail if this budget
|
|
11
|
+
(in USD) is exceeded". On 2026-09-28 an undeclared $60/day cap on `hf-tier-interactive` (made
|
|
12
|
+
through the API on 09-06, in no repo) 429'd every call of the claude2 fleet. So:
|
|
13
|
+
|
|
14
|
+
- `softBudget` is the prop to reach for: it "will NOT fail if this is exceeded" and fires the alert.
|
|
15
|
+
- `maxBudget` is **never defaulted**, and the plan is refused
|
|
16
|
+
(`LitellmBudgetHardCapUnjustifiedError`) unless `maxBudgetReason` is set. The reason lives in the
|
|
17
|
+
declaration, so a hard cap is a reviewed line of source, not an API side effect.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export class Interactive extends LiteLLMBudget('Interactive', {
|
|
21
|
+
budgetId: 'hf-tier-interactive', // adopts the live row (needs --adopt)
|
|
22
|
+
budgetDuration: '1d',
|
|
23
|
+
softBudget: 60,
|
|
24
|
+
rpmLimit: 60,
|
|
25
|
+
tpmLimit: 6_000_000,
|
|
26
|
+
}) {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Declaring an adopted hard tier **without** `maxBudget` clears the live cap: the update sends an
|
|
30
|
+
explicit `null` for every limit the declaration omits.
|
|
31
|
+
|
|
32
|
+
## Behaviour
|
|
33
|
+
|
|
34
|
+
| Concern | Rule |
|
|
35
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| Identity | `budgetId` (else a deterministic physical name). A different declared id is a `replace`. |
|
|
37
|
+
| Adopt | A live row with the id and no state is `Unowned`; needs `--adopt`. |
|
|
38
|
+
| Removal | `defaultRemovalPolicy: 'retain'`. Opt in with `RemovalPolicy.destroy()`. Delete is idempotent: a `BadRequest` is swallowed only if a re-list shows it gone. |
|
|
39
|
+
| Read | `/budget/list` (whole table). `rpm_limit`/`tpm_limit` come back as decimal strings (`BigInt?`), converted to numbers. |
|
|
40
|
+
| Write check | Reconcile reads back; a limit the row still carries that the declaration dropped fails with `LitellmBudgetFieldNotClearedError`. |
|
|
41
|
+
|
|
42
|
+
## Version: distilled copy is 1.100.0, the proxy is 1.103.0
|
|
43
|
+
|
|
44
|
+
The SDK is generated at v1.100.0. For the budget routes the request bodies used here
|
|
45
|
+
(`budget_id`, `soft_budget`, `max_budget`, `budget_duration`, `rpm_limit`, `tpm_limit`) are all in
|
|
46
|
+
that schema. **Not measured against 1.103.0** (no live proxy read was made, and the source for it
|
|
47
|
+
is not cloned): whether `/budget/update` accepts an explicit `null` to clear a limit, and what
|
|
48
|
+
happens on create of an existing id. The fake models 400 for both. The read-back check above
|
|
49
|
+
exists so an `exclude_none` merge fails loudly instead of leaving a hard cap in place.
|
|
50
|
+
|
|
51
|
+
## Not modelled
|
|
52
|
+
|
|
53
|
+
`max_parallel_requests`, `model_max_budget` and `budget_reset_at`: the row read back
|
|
54
|
+
(`BudgetListItem`) does not carry them, so a declaration could never be diffed. Add them once a
|
|
55
|
+
read that returns them is measured. Keys and teams that reference a tier are separate resources.
|
package/docs/litellm.md
CHANGED
|
@@ -16,12 +16,13 @@ against the tag's committed `schema.d.ts`; NOT a live read, because the referenc
|
|
|
16
16
|
Measured at 1.100.0 (`schema.prisma`, `pass_through_endpoints.py`, and the mini's own
|
|
17
17
|
`settings.yaml` `store_model_in_db: true`):
|
|
18
18
|
|
|
19
|
-
| Family | Managed by this resource?
|
|
20
|
-
| ------------------------------------------------------------------------------------------------------- |
|
|
21
|
-
| Pass-through endpoints | Yes — this package
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
19
|
+
| Family | Managed by this resource? |
|
|
20
|
+
| ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
21
|
+
| Pass-through endpoints | Yes — this package |
|
|
22
|
+
| Budgets (`/budget/*`) | Yes — `LiteLLM.Budget`, [litellm-budget.md](./litellm-budget.md) |
|
|
23
|
+
| Models, virtual keys, teams, credentials, access groups | DB-backed, API-managed, but no kit resource yet |
|
|
24
|
+
| `router_settings`, `litellm_settings`, `general_settings.master_key`/`database_url`/`store_model_in_db` | config-file only — read-only through the API |
|
|
25
|
+
| A `config.yaml` pass-through entry (`is_from_config: true`) | Read-only through this resource — see Refusals |
|
|
25
26
|
|
|
26
27
|
## Credentials
|
|
27
28
|
|
package/docs/postgres.md
CHANGED
|
@@ -123,3 +123,25 @@ mini's 19 already-live databases needs `adopt(true)` in the consuming stack.
|
|
|
123
123
|
|
|
124
124
|
Whether `ALTER DATABASE … REFRESH COLLATION VERSION` should ever be offered as an explicit,
|
|
125
125
|
separate operation (not part of `reconcile`) is open — nothing in this family calls it.
|
|
126
|
+
|
|
127
|
+
## Runner transport (loopback-only clusters)
|
|
128
|
+
|
|
129
|
+
A cluster reachable only by a command — CT100's rootful podman container `postgres`, loopback
|
|
130
|
+
listener, local socket, user `postgres`, no password — cannot take a wire connection from the
|
|
131
|
+
mini. `postgresRunnerProviders({ run, database, username, template? })` (or
|
|
132
|
+
`postgresRunnerConnection` on its own) runs the same `Postgres.Database` reconcile through a
|
|
133
|
+
caller-supplied `PsqlRunner`, which prepends the transport, e.g. `ssh <host> sudo -n podman
|
|
134
|
+
exec -i postgres <argv>` with `stdin` piped through. The kit holds no host, container or sudo
|
|
135
|
+
rule; the consuming stack's runner does.
|
|
136
|
+
|
|
137
|
+
- Same props, same create-and-assert, same adopt/retain and drift semantics — only the
|
|
138
|
+
`PgExecutor` differs (`psql-executor.ts`).
|
|
139
|
+
- `psql` has no bind parameters, so string params are inlined as `quoteStringLiteral`
|
|
140
|
+
literals; a non-string param is refused. Rows come back through `json_agg`, so `oid` and
|
|
141
|
+
`datconnlimit` are numbers as on the socket path.
|
|
142
|
+
- `CREATE DATABASE … TEMPLATE "template0"` is the runner default (`template` overrides):
|
|
143
|
+
CT100's `template1` is ParadeDB's and would copy postgis/pg_ivm/paradedb objects. The socket
|
|
144
|
+
path adds no `TEMPLATE` clause, unchanged.
|
|
145
|
+
- Not covered: there is no `Postgres.Role` resource in the kit yet, so the owner role must
|
|
146
|
+
exist (`PostgresDatabaseOwnerMissing` otherwise). NetBox's 11 lost triggers on `pg_restore`
|
|
147
|
+
are a restore concern, out of this resource's scope.
|
|
@@ -44,6 +44,51 @@ SdnVnet, SdnSubnet, HaResource, HaRule and CephFlag.
|
|
|
44
44
|
Their previous measured-none records
|
|
45
45
|
are historical inventories, not a current census or permission to skip migration.
|
|
46
46
|
|
|
47
|
+
**Measured 2026-09-28, by parser** (Bun's `Transpiler.scanImports` — a real import-graph
|
|
48
|
+
read, not a text match — walked one hop through local imports so a family that only
|
|
49
|
+
reaches the hand client via the shared `resource.ts` factory is still attributed
|
|
50
|
+
correctly; run against `packages/alchemy/src/proxmox` and `src/openbao`):
|
|
51
|
+
|
|
52
|
+
| Package | distilled-only | hand-client-only | mixed | no transport import (pure logic/form/test) |
|
|
53
|
+
| --------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
54
|
+
| `alchemy/src/proxmox` (165 files) | 71 | 9 — `resource.ts` (the shared `pveHandlers` factory itself), `sdn-zone.ts`, `sdn-vnet.ts`, `sdn-subnet.ts`, `sdn-apply.ts`, `sdn-apply-read.ts`, `ha-resource.ts`, `ha-rule.ts`, `ceph-flag.ts` | 1 — `index.ts` (barrel, re-exports both) | 84 |
|
|
55
|
+
| `alchemy/src/openbao` (80 files) | 2 | 36 (every `Bao*` family but Policy) | 5 — `distilled.ts`, `policy-wire.ts`, `policy-identity.ts`, `auth-role-wire.ts`, `auth-role-identity.ts` | 37 |
|
|
56
|
+
|
|
57
|
+
The proxmox count confirms the doc text above with one addition: nine, not eight,
|
|
58
|
+
files are hand-client-only. Eight back the seven named families (SdnApply's family
|
|
59
|
+
spans two files, `sdn-apply.ts` and `sdn-apply-read.ts`); the ninth is `resource.ts`
|
|
60
|
+
— the shared `pveHandlers`/`pveOperations` factory itself, which imports
|
|
61
|
+
`./client.ts` and no `@distilled.cloud/*` module, so
|
|
62
|
+
the one-hop rule that correctly attributes the seven named families' hand-client use
|
|
63
|
+
attributes this file's own import the same way. It does not mean every distilled-only
|
|
64
|
+
family still calls through it: 17 of the 71 import `resource.ts`, and all 17 are
|
|
65
|
+
type-only (`PveRequirements`/`WithTarget`, or `WithPbsTarget` for three PBS files —
|
|
66
|
+
types are erased at build time, invisible to `scanImports`, so the parser correctly
|
|
67
|
+
leaves them out of this table). `ceph-osd.ts` is one of those 17, not an exception;
|
|
68
|
+
it is the only one of the 71 that even mentions `pveHandlers` at all, in a comment
|
|
69
|
+
explaining why it deliberately does not use the factory — read in full, it calls
|
|
70
|
+
neither factory function. No other PVE/PBS family has regressed to hand-client
|
|
71
|
+
since the 28-family migration
|
|
72
|
+
landed.
|
|
73
|
+
|
|
74
|
+
**OpenBao is far less migrated than this repo's own comments could suggest** — only
|
|
75
|
+
`BaoPolicy`'s wire/identity pair and the `distilled.ts` wrapper use distilled transport
|
|
76
|
+
at all today; every other family (`BaoMount`, `BaoAuthMethod`, `BaoAuthRole`'s form
|
|
77
|
+
paths, `BaoPkiRole`, `BaoSshRole`, `BaoCloudflareRole`, `BaoProxmoxRole`, `BaoJwtRole`,
|
|
78
|
+
`BaoJwtAuthConfig`, `BaoMfaTotpMethod`, `BaoMfaLoginEnforcement`, `BaoPlugin`,
|
|
79
|
+
rename/remount/role-reconcile helpers) is still on the hand `bao-http.ts` client.
|
|
80
|
+
`mount-wire.ts` specifically is blocked on more than volume: the distilled SDK's
|
|
81
|
+
`mounts.ts` operations all declare `errors: []` (no typed absence error), unlike the
|
|
82
|
+
sibling `policies.ts`'s `PolicyNotFound` — a clean migration needs that SDK gap patched
|
|
83
|
+
first, in the distilled clone, per [the interim route](./distilled-interim.md)'s
|
|
84
|
+
build-SDK-first step. Two `codex/`-prefixed worktrees already exist in the distilled
|
|
85
|
+
clone (`openbao-mount-map`, `openbao-policy-runtime`) with unmerged work toward exactly
|
|
86
|
+
this: `openbao-mount-map`'s six commits generate the OpenBao SDK from a disposable local
|
|
87
|
+
2.6.2 server and add a typed mount-inventory response shape (`protocol-mount-tables.ts`,
|
|
88
|
+
provenance in `patches/mount-tables.md`), but its `mounts.ts` operations still declare
|
|
89
|
+
`errors: []` — the inventory-shape fix and the typed-absence-error fix are two separate
|
|
90
|
+
gaps, and only the first is done there.
|
|
91
|
+
|
|
47
92
|
- NetworkApply reads `listNodeNetwork`. A string `changes` sibling is preserved by
|
|
48
93
|
the protocol and decoded by that operation's union. The diff is counted and does
|
|
49
94
|
not enter state or logs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -128,12 +128,12 @@
|
|
|
128
128
|
"dependencies": {
|
|
129
129
|
"@distilled.cloud/caddy": "npm:@homeflare/distilled-caddy@0.2.2",
|
|
130
130
|
"@distilled.cloud/grafana": "npm:@homeflare/distilled-grafana@0.2.0",
|
|
131
|
-
"@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.
|
|
131
|
+
"@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.3.0",
|
|
132
132
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
133
133
|
"@distilled.cloud/openbao": "npm:@homeflare/distilled-openbao@0.2.2",
|
|
134
134
|
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.3.0",
|
|
135
135
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
136
|
-
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.
|
|
136
|
+
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.4",
|
|
137
137
|
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
|
|
138
138
|
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.4.0"
|
|
139
139
|
},
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed refusals `LiteLLM.Budget` raises on top of `@distilled.cloud/litellm`'s own SDK errors.
|
|
3
|
+
*/
|
|
4
|
+
import type * as budgets from '@distilled.cloud/litellm/budget_management';
|
|
5
|
+
import * as Data from 'effect/Data';
|
|
6
|
+
|
|
7
|
+
/** A `maxBudget` (a HARD cap: LiteLLM answers 429 once it is spent) declared with no reason. */
|
|
8
|
+
export class LitellmBudgetHardCapUnjustifiedError extends Data.TaggedError(
|
|
9
|
+
'LitellmBudgetHardCapUnjustifiedError',
|
|
10
|
+
)<{
|
|
11
|
+
readonly budgetId: string | undefined;
|
|
12
|
+
}> {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The write returned no error but the row still carries a limit the declaration does not.
|
|
16
|
+
* `/budget/update` may merge with `exclude_none` (the way `/config/pass_through_endpoint` does),
|
|
17
|
+
* in which case a limit can never be cleared by an update. Refused, never claimed as done.
|
|
18
|
+
*/
|
|
19
|
+
export class LitellmBudgetFieldNotClearedError extends Data.TaggedError(
|
|
20
|
+
'LitellmBudgetFieldNotClearedError',
|
|
21
|
+
)<{
|
|
22
|
+
readonly budgetId: string;
|
|
23
|
+
readonly fields: readonly string[];
|
|
24
|
+
}> {}
|
|
25
|
+
|
|
26
|
+
/** `/budget/info` answered something that is not a list of budget rows. */
|
|
27
|
+
export class LitellmBudgetUnreadableError extends Data.TaggedError('LitellmBudgetUnreadableError')<{
|
|
28
|
+
readonly budgetId: string;
|
|
29
|
+
}> {}
|
|
30
|
+
|
|
31
|
+
export type BudgetError =
|
|
32
|
+
| budgets.PostInfoBudgetBudgetInfoError
|
|
33
|
+
| budgets.PostNewBudgetBudgetNewError
|
|
34
|
+
| budgets.UpdateBudgetBudgetUpdatePostError
|
|
35
|
+
| budgets.DeleteBudgetBudgetDeletePostError
|
|
36
|
+
| LitellmBudgetHardCapUnjustifiedError
|
|
37
|
+
| LitellmBudgetFieldNotClearedError
|
|
38
|
+
| LitellmBudgetUnreadableError;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props, wire bodies and comparison for `LiteLLM.Budget`, testable without a server.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO SILENT `max_budget`. LiteLLM's own field doc: "Requests will fail if this budget (in USD)
|
|
5
|
+
* is exceeded" — a hard cap. On 2026-09-28 an undeclared $60/day `max_budget` on
|
|
6
|
+
* `hf-tier-interactive` 429'd the whole claude2 fleet. So `maxBudget` is never defaulted and is
|
|
7
|
+
* refused unless `maxBudgetReason` says why; `softBudget` is the monitoring field (it "will NOT
|
|
8
|
+
* fail if this is exceeded", it fires the alert).
|
|
9
|
+
* ⚠️ NOT MODELLED: `max_parallel_requests`, `model_max_budget`, `budget_reset_at`. The row read
|
|
10
|
+
* back (`BudgetListItem`, generated at LiteLLM 1.100.0) has none of them, so a declaration of
|
|
11
|
+
* them could never be diffed. Add them when a read that returns them is measured.
|
|
12
|
+
*/
|
|
13
|
+
import type * as budgets from '@distilled.cloud/litellm/budget_management';
|
|
14
|
+
|
|
15
|
+
export interface BudgetProps {
|
|
16
|
+
/** Live budget id. Set it to adopt an existing row (`hf-tier-agent`); default is a deterministic physical name. */
|
|
17
|
+
readonly budgetId?: string;
|
|
18
|
+
/** Alert threshold in USD. Requests keep flowing when exceeded. The monitoring field. */
|
|
19
|
+
readonly softBudget?: number;
|
|
20
|
+
/** HARD cap in USD: requests fail (429) once spent. Refused unless `maxBudgetReason` is set. */
|
|
21
|
+
readonly maxBudget?: number;
|
|
22
|
+
/** Why a hard cap is wanted. Required with `maxBudget`; recorded in the declaration only. */
|
|
23
|
+
readonly maxBudgetReason?: string;
|
|
24
|
+
/** Reset period, e.g. `'1d'`, `'28d'`. */
|
|
25
|
+
readonly budgetDuration?: string;
|
|
26
|
+
/** Requests per minute. */
|
|
27
|
+
readonly rpmLimit?: number;
|
|
28
|
+
/** Tokens per minute. */
|
|
29
|
+
readonly tpmLimit?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BudgetAttributes {
|
|
33
|
+
readonly budgetId: string;
|
|
34
|
+
readonly softBudget: number | null;
|
|
35
|
+
readonly maxBudget: number | null;
|
|
36
|
+
readonly budgetDuration: string | null;
|
|
37
|
+
readonly rpmLimit: number | null;
|
|
38
|
+
readonly tpmLimit: number | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Declared field → wire field. The single list every comparison and body walks. */
|
|
42
|
+
const FIELDS = [
|
|
43
|
+
['softBudget', 'soft_budget'],
|
|
44
|
+
['maxBudget', 'max_budget'],
|
|
45
|
+
['budgetDuration', 'budget_duration'],
|
|
46
|
+
['rpmLimit', 'rpm_limit'],
|
|
47
|
+
['tpmLimit', 'tpm_limit'],
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export const isHardCapUnjustified = (props: BudgetProps): boolean =>
|
|
51
|
+
props.maxBudget !== undefined && (props.maxBudgetReason ?? '').trim() === '';
|
|
52
|
+
|
|
53
|
+
/** Create body: only what is declared. */
|
|
54
|
+
export const createBody = (
|
|
55
|
+
props: BudgetProps,
|
|
56
|
+
budgetId: string,
|
|
57
|
+
): budgets.PostNewBudgetBudgetNewRequest => {
|
|
58
|
+
const body: Record<string, unknown> = { budget_id: budgetId };
|
|
59
|
+
for (const [prop, wire] of FIELDS) if (props[prop] !== undefined) body[wire] = props[prop];
|
|
60
|
+
return body as budgets.PostNewBudgetBudgetNewRequest;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Update body: declared fields, plus an explicit `null` for a live limit the declaration dropped. */
|
|
64
|
+
export const updateBody = (
|
|
65
|
+
props: BudgetProps,
|
|
66
|
+
live: BudgetAttributes,
|
|
67
|
+
): budgets.UpdateBudgetBudgetUpdatePostRequest => {
|
|
68
|
+
const body: Record<string, unknown> = { budget_id: live.budgetId };
|
|
69
|
+
for (const [prop, wire] of FIELDS) {
|
|
70
|
+
if (props[prop] !== undefined) body[wire] = props[prop];
|
|
71
|
+
else if (live[prop] !== null) body[wire] = null;
|
|
72
|
+
}
|
|
73
|
+
return body as budgets.UpdateBudgetBudgetUpdatePostRequest;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Wire names of the fields where live and declared disagree. */
|
|
77
|
+
export const differing = (live: BudgetAttributes, props: BudgetProps): readonly string[] =>
|
|
78
|
+
FIELDS.filter(([prop]) => live[prop] !== (props[prop] ?? null)).map(([, wire]) => wire);
|
|
79
|
+
|
|
80
|
+
const numberOrNull = (value: unknown): number | null =>
|
|
81
|
+
value === null || value === undefined || value === '' ? null : Number(value);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One row of `/budget/info`. `tpm_limit`/`rpm_limit` are `BigInt?` in LiteLLM's schema and come
|
|
85
|
+
* back as decimal strings (the generated `BudgetListItem` says so), hence `Number()`.
|
|
86
|
+
*/
|
|
87
|
+
export const toAttributes = (row: Record<string, unknown>): BudgetAttributes => ({
|
|
88
|
+
budgetDuration: typeof row['budget_duration'] === 'string' ? row['budget_duration'] : null,
|
|
89
|
+
budgetId: String(row['budget_id']),
|
|
90
|
+
maxBudget: numberOrNull(row['max_budget']),
|
|
91
|
+
rpmLimit: numberOrNull(row['rpm_limit']),
|
|
92
|
+
softBudget: numberOrNull(row['soft_budget']),
|
|
93
|
+
tpmLimit: numberOrNull(row['tpm_limit']),
|
|
94
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four LiteLLM `/budget/*` calls `LiteLLM.Budget` uses, through the SDK's typed operations.
|
|
3
|
+
*
|
|
4
|
+
* ★ READS GO THROUGH `/budget/list` (the whole table, a bare array), not `/budget/info`. Both
|
|
5
|
+
* answer `body: unknown` in the generated schema, and what `/budget/info` does for an id that is
|
|
6
|
+
* not there is unmeasured; a list answers absence unambiguously.
|
|
7
|
+
* ⚠️ Unlike pass-through endpoints, each budget is its own DB row, so no semaphore.
|
|
8
|
+
* ⚠️ DELETE is idempotent BY THIS FUNCTION: a `BadRequest` re-lists and is swallowed only when the
|
|
9
|
+
* id is genuinely absent, never on message text (pass-through `operations.ts`' rule).
|
|
10
|
+
*/
|
|
11
|
+
import * as budgets from '@distilled.cloud/litellm/budget_management';
|
|
12
|
+
import * as Effect from 'effect/Effect';
|
|
13
|
+
import { type LitellmOpContext, throughFetch } from './operations.ts';
|
|
14
|
+
import { type BudgetAttributes, toAttributes } from './budget-form.ts';
|
|
15
|
+
import { LitellmBudgetUnreadableError } from './budget-errors.ts';
|
|
16
|
+
|
|
17
|
+
export const listBudgets = (): Effect.Effect<
|
|
18
|
+
readonly BudgetAttributes[],
|
|
19
|
+
budgets.ListBudgetBudgetListGetError | LitellmBudgetUnreadableError,
|
|
20
|
+
LitellmOpContext
|
|
21
|
+
> =>
|
|
22
|
+
throughFetch(budgets.listBudgetBudgetListGet({})).pipe(
|
|
23
|
+
// ⚠️ MEASURED with the fake: the SDK types this `{ body: unknown }` but hands a bare JSON array
|
|
24
|
+
// back as the array itself, so accept both shapes and refuse anything else.
|
|
25
|
+
Effect.flatMap((response) => {
|
|
26
|
+
const rows: unknown = Array.isArray(response)
|
|
27
|
+
? response
|
|
28
|
+
: (response as { body?: unknown }).body;
|
|
29
|
+
return Array.isArray(rows)
|
|
30
|
+
? Effect.succeed(rows.map((row) => toAttributes(row as Record<string, unknown>)))
|
|
31
|
+
: Effect.fail(new LitellmBudgetUnreadableError({ budgetId: '*' }));
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const createBudget = (body: budgets.PostNewBudgetBudgetNewRequest) =>
|
|
36
|
+
throughFetch(budgets.postNewBudgetBudgetNew(body)).pipe(Effect.asVoid);
|
|
37
|
+
|
|
38
|
+
export const updateBudget = (body: budgets.UpdateBudgetBudgetUpdatePostRequest) =>
|
|
39
|
+
throughFetch(budgets.updateBudgetBudgetUpdatePost(body)).pipe(Effect.asVoid);
|
|
40
|
+
|
|
41
|
+
export const deleteBudget = (id: string) =>
|
|
42
|
+
throughFetch(budgets.deleteBudgetBudgetDeletePost({ id })).pipe(
|
|
43
|
+
Effect.asVoid,
|
|
44
|
+
Effect.catchTag('BadRequest', (original) =>
|
|
45
|
+
listBudgets().pipe(
|
|
46
|
+
Effect.flatMap((rows) =>
|
|
47
|
+
rows.some((row) => row.budgetId === id) ? Effect.fail(original) : Effect.void,
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
);
|