@homeflare/alchemy 0.28.0 → 0.29.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 (210) hide show
  1. package/README.md +2 -0
  2. package/dist/caddy/admin-address.d.ts +27 -0
  3. package/dist/caddy/admin-address.d.ts.map +1 -0
  4. package/dist/caddy/admin-calls.d.ts +25 -28
  5. package/dist/caddy/admin-calls.d.ts.map +1 -1
  6. package/dist/caddy/admin.d.ts +28 -38
  7. package/dist/caddy/admin.d.ts.map +1 -1
  8. package/dist/caddy/caddy-http-client.d.ts +25 -0
  9. package/dist/caddy/caddy-http-client.d.ts.map +1 -0
  10. package/dist/caddy/config-errors.d.ts +24 -0
  11. package/dist/caddy/config-errors.d.ts.map +1 -0
  12. package/dist/caddy/config-lifecycle.d.ts +20 -49
  13. package/dist/caddy/config-lifecycle.d.ts.map +1 -1
  14. package/dist/caddy/config-reconcile.d.ts +45 -0
  15. package/dist/caddy/config-reconcile.d.ts.map +1 -0
  16. package/dist/caddy/config.d.ts +4 -3
  17. package/dist/caddy/config.d.ts.map +1 -1
  18. package/dist/caddy/fake-caddy.d.ts +10 -2
  19. package/dist/caddy/fake-caddy.d.ts.map +1 -1
  20. package/dist/caddy/index.d.ts +3 -3
  21. package/dist/caddy/index.d.ts.map +1 -1
  22. package/dist/caddy/index.js +487 -444
  23. package/dist/caddy/index.js.map +16 -12
  24. package/dist/caddy/local-admin.d.ts +20 -34
  25. package/dist/caddy/local-admin.d.ts.map +1 -1
  26. package/dist/caddy/providers.d.ts +19 -3
  27. package/dist/caddy/providers.d.ts.map +1 -1
  28. package/dist/cloudflare/mesh-node-token.d.ts +1 -1
  29. package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
  30. package/dist/forgejo/branch-protection.d.ts +4 -4
  31. package/dist/forgejo/org-actions-secrets.d.ts +4 -4
  32. package/dist/forgejo/org-label.d.ts +4 -4
  33. package/dist/forgejo/org-team.d.ts +4 -4
  34. package/dist/forgejo/repo-webhook.d.ts +4 -4
  35. package/dist/forgejo/repository.d.ts +4 -4
  36. package/dist/forgejo/team-member.d.ts +4 -4
  37. package/dist/github/declare-repo-baseline.d.ts +1 -1
  38. package/dist/github/declare-repo-baseline.d.ts.map +1 -1
  39. package/dist/github/index.d.ts +8 -4
  40. package/dist/github/index.d.ts.map +1 -1
  41. package/dist/github/index.js +63 -23
  42. package/dist/github/index.js.map +13 -12
  43. package/dist/github/repo-baseline-data.d.ts +10 -1
  44. package/dist/github/repo-baseline-data.d.ts.map +1 -1
  45. package/dist/github/repo-policy.d.ts +1 -1
  46. package/dist/github/repo-policy.d.ts.map +1 -1
  47. package/dist/github/repository-ruleset-errors.d.ts +3 -2
  48. package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
  49. package/dist/github/repository-ruleset-form.d.ts.map +1 -1
  50. package/dist/github/repository-ruleset-guards.d.ts +3 -2
  51. package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
  52. package/dist/github/repository-ruleset-octokit.d.ts +49 -0
  53. package/dist/github/repository-ruleset-octokit.d.ts.map +1 -1
  54. package/dist/github/repository-ruleset-probe.d.ts +2 -1
  55. package/dist/github/repository-ruleset-probe.d.ts.map +1 -1
  56. package/dist/github/repository-ruleset-props.d.ts +7 -4
  57. package/dist/github/repository-ruleset-props.d.ts.map +1 -1
  58. package/dist/github/repository-ruleset-providers.d.ts +42 -0
  59. package/dist/github/repository-ruleset-providers.d.ts.map +1 -0
  60. package/dist/github/repository-ruleset.d.ts +26 -4
  61. package/dist/github/repository-ruleset.d.ts.map +1 -1
  62. package/dist/litellm/fake-litellm.d.ts +27 -16
  63. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  64. package/dist/litellm/fake-stack.d.ts +9 -4
  65. package/dist/litellm/fake-stack.d.ts.map +1 -1
  66. package/dist/litellm/index.d.ts +7 -5
  67. package/dist/litellm/index.d.ts.map +1 -1
  68. package/dist/litellm/index.js +53 -123
  69. package/dist/litellm/index.js.map +8 -8
  70. package/dist/litellm/operations.d.ts +60 -0
  71. package/dist/litellm/operations.d.ts.map +1 -0
  72. package/dist/litellm/pass-through-endpoint.d.ts +10 -28
  73. package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
  74. package/dist/litellm/pass-through-errors.d.ts +37 -0
  75. package/dist/litellm/pass-through-errors.d.ts.map +1 -0
  76. package/dist/litellm/pass-through-form.d.ts +11 -4
  77. package/dist/litellm/pass-through-form.d.ts.map +1 -1
  78. package/dist/litellm/providers.d.ts +7 -5
  79. package/dist/litellm/providers.d.ts.map +1 -1
  80. package/dist/netbox/prefix.d.ts +4 -4
  81. package/dist/paperless/custom-field.d.ts +21 -21
  82. package/dist/paperless/custom-field.d.ts.map +1 -1
  83. package/dist/paperless/document-type.d.ts +34 -4
  84. package/dist/paperless/document-type.d.ts.map +1 -1
  85. package/dist/paperless/fake-paperless.d.ts +36 -19
  86. package/dist/paperless/fake-paperless.d.ts.map +1 -1
  87. package/dist/paperless/index.d.ts +7 -2
  88. package/dist/paperless/index.d.ts.map +1 -1
  89. package/dist/paperless/index.js +164 -228
  90. package/dist/paperless/index.js.map +10 -13
  91. package/dist/paperless/matching-locate.d.ts +42 -20
  92. package/dist/paperless/matching-locate.d.ts.map +1 -1
  93. package/dist/paperless/matching-types.d.ts +47 -18
  94. package/dist/paperless/matching-types.d.ts.map +1 -1
  95. package/dist/paperless/matching.d.ts +30 -27
  96. package/dist/paperless/matching.d.ts.map +1 -1
  97. package/dist/paperless/providers.d.ts +13 -7
  98. package/dist/paperless/providers.d.ts.map +1 -1
  99. package/dist/paperless/storage-path.d.ts +34 -4
  100. package/dist/paperless/storage-path.d.ts.map +1 -1
  101. package/dist/paperless/tag.d.ts +43 -8
  102. package/dist/paperless/tag.d.ts.map +1 -1
  103. package/dist/proxmox/acl.d.ts +30 -23
  104. package/dist/proxmox/acl.d.ts.map +1 -1
  105. package/dist/proxmox/distilled-pve.d.ts +89 -0
  106. package/dist/proxmox/distilled-pve.d.ts.map +1 -0
  107. package/dist/proxmox/index.js +663 -609
  108. package/dist/proxmox/index.js.map +12 -11
  109. package/dist/proxmox/members.d.ts +29 -10
  110. package/dist/proxmox/members.d.ts.map +1 -1
  111. package/docs/caddy-admin.md +1 -1
  112. package/docs/caddy.md +3 -2
  113. package/docs/credentials.md +16 -0
  114. package/docs/litellm.md +21 -13
  115. package/docs/paperless.md +12 -0
  116. package/docs/peers.md +5 -0
  117. package/docs/repo-policy.md +16 -1
  118. package/docs/repository-ruleset.md +53 -13
  119. package/docs/upstream-conformance.md +126 -10
  120. package/package.json +5 -2
  121. package/src/caddy/admin-address.ts +71 -0
  122. package/src/caddy/admin-calls.ts +48 -100
  123. package/src/caddy/admin.ts +37 -41
  124. package/src/caddy/adopt-scope.test.ts +1 -1
  125. package/src/caddy/caddy-http-client.ts +229 -0
  126. package/src/caddy/config-adopt-provider.test.ts +139 -0
  127. package/src/caddy/config-adopt.test.ts +60 -126
  128. package/src/caddy/config-errors.ts +47 -0
  129. package/src/caddy/config-lifecycle.test.ts +55 -42
  130. package/src/caddy/config-lifecycle.ts +72 -162
  131. package/src/caddy/config-reconcile.ts +138 -0
  132. package/src/caddy/config.ts +36 -42
  133. package/src/caddy/fake-caddy.ts +20 -2
  134. package/src/caddy/index.ts +3 -8
  135. package/src/caddy/local-admin.test.ts +35 -21
  136. package/src/caddy/local-admin.ts +60 -167
  137. package/src/caddy/providers.test.ts +14 -6
  138. package/src/caddy/providers.ts +20 -4
  139. package/src/github/declare-repo-baseline.ts +6 -6
  140. package/src/github/index.ts +8 -4
  141. package/src/github/repo-baseline-data.test.ts +13 -0
  142. package/src/github/repo-baseline-data.ts +11 -2
  143. package/src/github/repo-policy-stack.test.ts +59 -0
  144. package/src/github/repo-policy.ts +3 -2
  145. package/src/github/repository-ruleset-errors.ts +6 -4
  146. package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
  147. package/src/github/repository-ruleset-exact-adopt.test.ts +14 -9
  148. package/src/github/repository-ruleset-form.ts +7 -3
  149. package/src/github/repository-ruleset-guards.ts +3 -2
  150. package/src/github/repository-ruleset-octokit.test.ts +141 -0
  151. package/src/github/repository-ruleset-octokit.ts +121 -30
  152. package/src/github/repository-ruleset-probe.ts +2 -1
  153. package/src/github/repository-ruleset-props.ts +7 -4
  154. package/src/github/repository-ruleset-providers.ts +46 -0
  155. package/src/github/repository-ruleset-wire.test.ts +38 -27
  156. package/src/github/repository-ruleset.ts +27 -3
  157. package/src/litellm/fake-litellm.ts +83 -78
  158. package/src/litellm/fake-stack.ts +13 -5
  159. package/src/litellm/index.ts +7 -21
  160. package/src/litellm/operations.ts +149 -0
  161. package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +17 -20
  162. package/src/litellm/pass-through-endpoint-replace.test.ts +3 -4
  163. package/src/litellm/pass-through-endpoint.test.ts +8 -8
  164. package/src/litellm/pass-through-endpoint.ts +21 -31
  165. package/src/litellm/pass-through-errors.ts +37 -0
  166. package/src/litellm/pass-through-form.ts +20 -10
  167. package/src/litellm/providers.ts +8 -7
  168. package/src/paperless/custom-field.test.ts +49 -40
  169. package/src/paperless/custom-field.ts +57 -33
  170. package/src/paperless/document-type.test.ts +48 -0
  171. package/src/paperless/document-type.ts +51 -22
  172. package/src/paperless/fake-paperless.ts +63 -72
  173. package/src/paperless/index.ts +7 -13
  174. package/src/paperless/matching-locate.test.ts +102 -0
  175. package/src/paperless/matching-locate.ts +80 -86
  176. package/src/paperless/matching-types.ts +49 -16
  177. package/src/paperless/matching.ts +77 -90
  178. package/src/paperless/providers.ts +14 -10
  179. package/src/paperless/storage-path.test.ts +56 -0
  180. package/src/paperless/storage-path.ts +53 -24
  181. package/src/paperless/tag-identity.test.ts +127 -0
  182. package/src/paperless/tag.test.ts +100 -0
  183. package/src/paperless/tag.ts +62 -32
  184. package/src/proxmox/acl.test.ts +145 -0
  185. package/src/proxmox/acl.ts +128 -150
  186. package/src/proxmox/distilled-pve.test.ts +231 -0
  187. package/src/proxmox/distilled-pve.ts +147 -0
  188. package/src/proxmox/members-timeout.test.ts +161 -0
  189. package/src/proxmox/members.test.ts +9 -0
  190. package/src/proxmox/members.ts +36 -2
  191. package/dist/litellm/client.d.ts +0 -76
  192. package/dist/litellm/client.d.ts.map +0 -1
  193. package/dist/litellm/credentials.d.ts +0 -48
  194. package/dist/litellm/credentials.d.ts.map +0 -1
  195. package/dist/litellm/generated/pass-through.d.ts +0 -71
  196. package/dist/litellm/generated/pass-through.d.ts.map +0 -1
  197. package/dist/paperless/client.d.ts +0 -37
  198. package/dist/paperless/client.d.ts.map +0 -1
  199. package/dist/paperless/credentials.d.ts +0 -40
  200. package/dist/paperless/credentials.d.ts.map +0 -1
  201. package/dist/paperless/errors.d.ts +0 -49
  202. package/dist/paperless/errors.d.ts.map +0 -1
  203. package/src/litellm/client.ts +0 -195
  204. package/src/litellm/credentials.ts +0 -73
  205. package/src/litellm/generated/pass-through.ts +0 -73
  206. package/src/paperless/client.ts +0 -88
  207. package/src/paperless/credentials.ts +0 -69
  208. package/src/paperless/errors.ts +0 -62
  209. package/src/paperless/matching-identity.test.ts +0 -194
  210. package/src/paperless/matching.test.ts +0 -128
@@ -2,32 +2,18 @@
2
2
  * LiteLLM providers for Alchemy.
3
3
  *
4
4
  * ⛔ THIS BARREL IS THE PUBLIC API, deliberately smaller than the directory — `fake-litellm.ts`
5
- * and the internals `pass-through-form.ts`/`client.ts` need but a consumer should not depend on
6
- * directly are not re-exported here (netbox/index.ts's own rule).
5
+ * and the internals `pass-through-form.ts`/`operations.ts` need but a consumer should not depend
6
+ * on directly are not re-exported here (netbox/index.ts's own rule).
7
+ * ★ CREDENTIALS AND TYPES NOW COME FROM `@distilled.cloud/litellm` ITSELF (2026-09-24, moved off a
8
+ * hand-rolled `client.ts`/`credentials.ts` the same way `/netbox` and `/forgejo` did) — a
9
+ * consumer building its own credentials layer imports `Credentials`/`CredentialsFromEnv`/
10
+ * `credentials` from `@distilled.cloud/litellm/Credentials` directly, not through this barrel.
7
11
  */
8
- export {
9
- LITELLM_PROXY_API_KEY_ENV,
10
- LITELLM_PROXY_URL_ENV,
11
- LitellmCredentials,
12
- type LitellmCreds,
13
- LitellmCredentialsError,
14
- litellmCredentialsLayer,
15
- litellmCredentialsLayerFor,
16
- } from './credentials.ts';
17
- export {
18
- LITELLM_OPERATIONS,
19
- LitellmBadRequestError,
20
- type LitellmError,
21
- LitellmHttpError,
22
- type LitellmRequirements,
23
- LitellmTransportError,
24
- LitellmUnauthorizedError,
25
- } from './client.ts';
26
12
  export type {
27
13
  PassThroughEndpointResponse,
28
14
  PassThroughGenericEndpoint,
29
15
  PassThroughGuardrailSettings,
30
- } from './generated/pass-through.ts';
16
+ } from '@distilled.cloud/litellm/misc';
31
17
  export {
32
18
  LiteLLMPassThroughEndpoint,
33
19
  LiteLLMPassThroughEndpointProvider,
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The four LiteLLM `/config/pass_through_endpoint*` calls this package writes to and reads from —
3
+ * now through `@distilled.cloud/litellm`'s typed operations (S23), not a hand-rolled `HttpClient`
4
+ * client. `client.ts` is retired; nothing else in this package imported it.
5
+ *
6
+ * ⛔ STILL WRAPPED IN A PER-BASE-URL SEMAPHORE, UNCHANGED FROM `client.ts`. LiteLLM stores every
7
+ * pass-through endpoint in ONE `general_settings.pass_through_endpoints` list
8
+ * (`pass_through_endpoints.py` at tag v1.100.0, mirrored in the SDK's own `misc.ts` docstrings),
9
+ * so create/update/delete are each a read-modify-write of the WHOLE list through
10
+ * `update_config_general_settings`. Two of this package's own calls racing each other — two
11
+ * endpoints created in the same deploy — would each read the list, add their own row and write
12
+ * it back, and the second write would silently drop the first row. The SDK has no opinion on
13
+ * this: it is a quirk of how THIS vendor stores the resource, not a distilled protocol concern,
14
+ * so it stays here, one layer above the typed calls. One permit per proxy serialises this
15
+ * package's own writes; it cannot serialise against the LiteLLM UI or a second deploy process,
16
+ * which is why `pass-through-endpoint.ts`'s `reconcile` reads back after writing rather than
17
+ * trusting the call that just returned (docs/litellm.md).
18
+ * ⛔ NO STATUS SNIFFING (S21). Every failure the SDK's own typed operations declare — `BadRequest`
19
+ * (400), `NotFound` (404 on update only), `UnprocessableEntity` (422) plus the shared
20
+ * `Unauthorized`/`TooManyRequests`/`InternalServerError`/`BadGateway`/`ServiceUnavailable`/
21
+ * `GatewayTimeout` every operation carries — is a distilled patch's doing
22
+ * (`@distilled.cloud/litellm`'s own `patches/misc/*.json`, applied at generation time), never a
23
+ * status check here or in `pass-through-endpoint.ts`. Deleting an id LiteLLM does not have
24
+ * answers `BadRequest` (measured against the tag's `pass_through_endpoint_delete` — the id is
25
+ * looked up in the list and a miss raises `HTTPException(400)`, not 404) — but so does a
26
+ * genuinely-live row when the caller isn't PROXY_ADMIN, or the DB is disconnected. Telling those
27
+ * apart by re-listing, not by the `BadRequest`'s message text, is `pass-through-endpoint.ts`'s
28
+ * `deletePassThroughEndpoint`'s own job (S21 — a status-and-a-real-read decision, never body
29
+ * text) — this file only forwards the typed error.
30
+ */
31
+ import * as misc from '@distilled.cloud/litellm/misc';
32
+ import { Credentials } from '@distilled.cloud/litellm/Credentials';
33
+ import type { LitellmOpContext } from '@distilled.cloud/litellm/Protocol';
34
+ import type { ConfigError } from '@distilled.cloud/litellm/Errors';
35
+ import * as Effect from 'effect/Effect';
36
+ import * as Semaphore from 'effect/Semaphore';
37
+
38
+ export type { LitellmOpContext } from '@distilled.cloud/litellm/Protocol';
39
+ export type LitellmRequirements = LitellmOpContext;
40
+ export type {
41
+ PassThroughEndpointResponse,
42
+ PassThroughGenericEndpoint,
43
+ PassThroughGuardrailSettings,
44
+ } from '@distilled.cloud/litellm/misc';
45
+
46
+ const semaphores = new Map<string, Semaphore.Semaphore>();
47
+
48
+ /** One semaphore per base URL, created on first use. Never `Hash.structure` (S18) — a plain Map key. */
49
+ const semaphoreFor = (baseUrl: string): Effect.Effect<Semaphore.Semaphore> =>
50
+ Effect.sync(() => {
51
+ const existing = semaphores.get(baseUrl);
52
+ if (existing !== undefined) return existing;
53
+ const created = Semaphore.makeUnsafe(1);
54
+ semaphores.set(baseUrl, created);
55
+ return created;
56
+ });
57
+
58
+ /**
59
+ * Serialises a write against this call's own base URL — see the file header.
60
+ *
61
+ * Resolving `Credentials` can itself fail typed (`ConfigError` — S20/S24, credentials.ts), so
62
+ * the return type carries that alongside the wrapped operation's own error `E`; every call
63
+ * site's declared error type already includes `ConfigError` as a member of the SDK's shared
64
+ * `LitellmOpError` (protocol.ts), so this widening is a no-op for callers.
65
+ */
66
+ const mutate = <A, E>(
67
+ io: Effect.Effect<A, E, LitellmOpContext>,
68
+ ): Effect.Effect<A, E | ConfigError, LitellmOpContext> =>
69
+ Effect.gen(function* () {
70
+ const resolve = yield* Credentials;
71
+ const creds = yield* resolve;
72
+ const semaphore = yield* semaphoreFor(creds.apiBaseUrl);
73
+ return yield* semaphore.withPermits(1)(io);
74
+ });
75
+
76
+ /** GET /config/pass_through_endpoint — every row, config-file and DB alike. Not under the semaphore: reads don't race the field. */
77
+ export const listPassThroughEndpoints = (): Effect.Effect<
78
+ readonly misc.PassThroughGenericEndpoint[],
79
+ misc.GetPassThroughEndpointsConfigPassThroughEndpointGetError,
80
+ LitellmOpContext
81
+ > =>
82
+ misc
83
+ .getPassThroughEndpointsConfigPassThroughEndpointGet({})
84
+ .pipe(Effect.map((response) => response.endpoints));
85
+
86
+ /** POST /config/pass_through_endpoint. `body` carries `id` — see pass-through-form.ts. */
87
+ export const createPassThroughEndpoint = (
88
+ body: misc.PassThroughGenericEndpoint,
89
+ ): Effect.Effect<
90
+ void,
91
+ misc.CreatePassThroughEndpointsConfigPassThroughEndpointPostError,
92
+ LitellmOpContext
93
+ > => mutate(misc.createPassThroughEndpointsConfigPassThroughEndpointPost(body).pipe(Effect.asVoid));
94
+
95
+ /**
96
+ * POST /config/pass_through_endpoint/{endpoint_id}. Merges with `exclude_none` — see
97
+ * pass-through-form.ts. `body` is `pass-through-form.ts`'s own `UpdateBody`, not a blanket
98
+ * `Partial<>` — `path`/`target` are the two fields the SDK's request type still requires
99
+ * non-optionally on an update.
100
+ */
101
+ export const updatePassThroughEndpoint = (
102
+ endpointId: string,
103
+ body: Partial<Omit<misc.PassThroughGenericEndpoint, 'path' | 'target'>> &
104
+ Pick<misc.PassThroughGenericEndpoint, 'path' | 'target'>,
105
+ ): Effect.Effect<
106
+ void,
107
+ misc.UpdatePassThroughEndpointsConfigPassThroughEndpointEndpointIdPostError,
108
+ LitellmOpContext
109
+ > =>
110
+ mutate(
111
+ misc
112
+ .updatePassThroughEndpointsConfigPassThroughEndpointEndpointIdPost({
113
+ ...body,
114
+ endpoint_id: endpointId,
115
+ })
116
+ .pipe(Effect.asVoid),
117
+ );
118
+
119
+ /**
120
+ * DELETE /config/pass_through_endpoint?endpoint_id=… — idempotent BY THIS FUNCTION, not by the
121
+ * vendor. ⚠️ Measured at v1.100.0: deleting an id the list does not have raises `HTTPException(400)`
122
+ * from `pass_through_endpoint_delete`, which the SDK decodes as a typed `BadRequest` — but the SAME
123
+ * route also raises the SAME `BadRequest` for `not_allowed_access` (a non-PROXY_ADMIN caller) or a
124
+ * disconnected DB, on a row that never got deleted. A `BadRequest` alone cannot tell the two apart,
125
+ * so THIS FUNCTION DOESN'T GUESS FROM THE TAG ALONE: on `BadRequest` it re-lists (a GET — no
126
+ * PROXY_ADMIN required) and only treats the delete as already-done if `endpointId` is genuinely
127
+ * absent; otherwise the delete failed and this re-fails with the ORIGINAL error, not a message
128
+ * match (S21 — a status-and-a-real-read decision, never body text).
129
+ */
130
+ export const deletePassThroughEndpoint = (
131
+ endpointId: string,
132
+ ): Effect.Effect<
133
+ void,
134
+ misc.DeletePassThroughEndpointsConfigPassThroughEndpointDeleteError,
135
+ LitellmOpContext
136
+ > =>
137
+ mutate(
138
+ misc
139
+ .deletePassThroughEndpointsConfigPassThroughEndpointDelete({ endpoint_id: endpointId })
140
+ .pipe(
141
+ Effect.asVoid,
142
+ Effect.catchTag('BadRequest', (original) =>
143
+ Effect.gen(function* () {
144
+ const rows = yield* listPassThroughEndpoints();
145
+ if (rows.some((row) => row.id === endpointId)) return yield* Effect.fail(original);
146
+ }),
147
+ ),
148
+ ),
149
+ );
@@ -13,15 +13,16 @@
13
13
  * "already gone" only if the row is actually absent.
14
14
  */
15
15
  import { describe, expect, test } from 'bun:test';
16
+ import { credentials } from '@distilled.cloud/litellm/Credentials';
16
17
  import * as Effect from 'effect/Effect';
18
+ import * as Layer from 'effect/Layer';
17
19
  import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
20
+ import { FAKE_BASE, startFakeLitellm } from './fake-litellm.ts';
18
21
  import {
19
22
  type LitellmRequirements,
20
23
  deletePassThroughEndpoint,
21
24
  listPassThroughEndpoints,
22
- } from './client.ts';
23
- import { litellmCredentialsLayerFor } from './credentials.ts';
24
- import { startFakeLitellm } from './fake-litellm.ts';
25
+ } from './operations.ts';
25
26
 
26
27
  const MASTER_KEY = 'sk-test-master';
27
28
  const PROPS = {
@@ -32,35 +33,31 @@ const PROPS = {
32
33
  target: 'https://api.bria.ai',
33
34
  };
34
35
 
35
- const run = <A, E>(baseUrl: string, effect: Effect.Effect<A, E, LitellmRequirements>): Promise<A> =>
36
+ const run = <A, E>(
37
+ fetchFn: typeof globalThis.fetch,
38
+ effect: Effect.Effect<A, E, LitellmRequirements>,
39
+ ): Promise<A> =>
36
40
  Effect.runPromise(
37
41
  effect.pipe(
38
42
  Effect.provide(FetchHttpClient.layer),
39
- Effect.provide(litellmCredentialsLayerFor({ apiKey: MASTER_KEY, baseUrl })),
43
+ Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
44
+ Effect.provide(credentials({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE })),
40
45
  ) as Effect.Effect<A, E, never>,
41
46
  );
42
47
 
43
48
  describe('a delete forbidden for a reason other than "already gone"', () => {
44
49
  test('is not swallowed: it fails, and the row is still live', async () => {
45
50
  const fake = startFakeLitellm({ forbidDelete: true, masterKey: MASTER_KEY, seed: [PROPS] });
46
- try {
47
- const result = run(fake.url, deletePassThroughEndpoint('live-1'));
48
- await expect(result).rejects.toThrow();
49
- const rows = await run(fake.url, listPassThroughEndpoints());
50
- expect(rows.some((r) => r.id === 'live-1')).toBe(true);
51
- } finally {
52
- fake.stop();
53
- }
51
+ const result = run(fake.fetch, deletePassThroughEndpoint('live-1'));
52
+ await expect(result).rejects.toThrow();
53
+ const rows = await run(fake.fetch, listPassThroughEndpoints());
54
+ expect(rows.some((r) => r.id === 'live-1')).toBe(true);
54
55
  });
55
56
 
56
57
  test('a genuinely-gone id still succeeds with no error (idempotent delete preserved)', async () => {
57
58
  const fake = startFakeLitellm({ forbidDelete: true, masterKey: MASTER_KEY });
58
- try {
59
- await expect(
60
- run(fake.url, deletePassThroughEndpoint('never-existed')),
61
- ).resolves.toBeUndefined();
62
- } finally {
63
- fake.stop();
64
- }
59
+ await expect(
60
+ run(fake.fetch, deletePassThroughEndpoint('never-existed')),
61
+ ).resolves.toBeUndefined();
65
62
  });
66
63
  });
@@ -15,11 +15,11 @@
15
15
  * caught this: it calls `h.reconcile` directly with the SAME instanceId and the OLD output, a
16
16
  * call shape the real engine never produces for a `replace`.
17
17
  */
18
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
18
+ import { beforeEach, describe, expect, test } from 'bun:test';
19
19
  import * as RemovalPolicy from 'alchemy/RemovalPolicy';
20
20
  import * as Effect from 'effect/Effect';
21
+ import { FAKE_BASE, type FakeLitellm, startFakeLitellm } from './fake-litellm.ts';
21
22
  import { type FakeStack, fakeStack, writesOf } from './fake-stack.ts';
22
- import { type FakeLitellm, startFakeLitellm } from './fake-litellm.ts';
23
23
  import { LiteLLMPassThroughEndpoint } from './pass-through-endpoint.ts';
24
24
  import type { PassThroughEndpointProps } from './pass-through-form.ts';
25
25
 
@@ -29,9 +29,8 @@ let fake: FakeLitellm;
29
29
  let stack: FakeStack;
30
30
  beforeEach(() => {
31
31
  fake = startFakeLitellm({ masterKey: MASTER_KEY });
32
- stack = fakeStack({ apiKey: MASTER_KEY, baseUrl: fake.url });
32
+ stack = fakeStack({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE }, fake.fetch);
33
33
  });
34
- afterEach(() => fake.stop());
35
34
 
36
35
  /** ★ `RemovalPolicy.destroy()`, deliberately: under the (non-default) `retain`, `deleteFirst`'s own
37
36
  * old-generation delete is skipped by the engine (Apply.ts's `deleteOldGenerations`), so a path
@@ -2,15 +2,16 @@
2
2
  * `LiteLLM.PassThroughEndpoint`'s lifecycle handlers, called directly against `fake-litellm.ts` —
3
3
  * the loopback-fake pattern this kit uses in place of `alchemy/Test/*` (H12, no live writes).
4
4
  */
5
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
5
+ import { beforeEach, describe, expect, test } from 'bun:test';
6
+ import { credentials } from '@distilled.cloud/litellm/Credentials';
6
7
  import { Unowned } from 'alchemy/AdoptPolicy';
7
8
  import { Stack } from 'alchemy/Stack';
8
9
  import { Stage } from 'alchemy/Stage';
9
10
  import * as Effect from 'effect/Effect';
11
+ import * as Layer from 'effect/Layer';
10
12
  import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
11
- import { litellmCredentialsLayerFor } from './credentials.ts';
12
- import { type FakeLitellm, startFakeLitellm } from './fake-litellm.ts';
13
- import { createPassThroughEndpoint } from './client.ts';
13
+ import { FAKE_BASE, type FakeLitellm, startFakeLitellm } from './fake-litellm.ts';
14
+ import { createPassThroughEndpoint } from './operations.ts';
14
15
  import {
15
16
  LitellmConfigPathConflictError,
16
17
  LitellmLiteralSecretHeaderError,
@@ -22,11 +23,11 @@ const MASTER_KEY = 'sk-test-master';
22
23
  const INSTANCE_ID = 'a'.repeat(32);
23
24
  const FAKE_STACK = { actions: {}, bindings: {}, name: 's', resources: {}, stage: 'test' } as never;
24
25
 
26
+ /** No `.stop()` any more — `fake.fetch` is a plain function, no real socket to close. */
25
27
  let fake: FakeLitellm;
26
28
  beforeEach(() => {
27
29
  fake = startFakeLitellm({ masterKey: MASTER_KEY });
28
30
  });
29
- afterEach(() => fake.stop());
30
31
 
31
32
  const run = <A, E>(effect: Effect.Effect<A, E, never>): Promise<A> =>
32
33
  Effect.runPromise(effect as Effect.Effect<A, E, never>);
@@ -34,7 +35,8 @@ const run = <A, E>(effect: Effect.Effect<A, E, never>): Promise<A> =>
34
35
  const withServices = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
35
36
  effect.pipe(
36
37
  Effect.provide(FetchHttpClient.layer),
37
- Effect.provide(litellmCredentialsLayerFor({ apiKey: MASTER_KEY, baseUrl: fake.url })),
38
+ Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fake.fetch)),
39
+ Effect.provide(credentials({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE })),
38
40
  Effect.provideService(Stage, 'test'),
39
41
  Effect.provideService(Stack, FAKE_STACK),
40
42
  ) as Effect.Effect<A, E, never>;
@@ -150,7 +152,6 @@ describe('two endpoints created in one deploy', () => {
150
152
 
151
153
  describe('a path held by an is_from_config row', () => {
152
154
  test('is refused at read, before any write', async () => {
153
- fake.stop();
154
155
  fake = startFakeLitellm({
155
156
  masterKey: MASTER_KEY,
156
157
  seed: [{ ...PROPS, id: 'cfg-1', is_from_config: true }],
@@ -167,7 +168,6 @@ describe('a path held by an is_from_config row', () => {
167
168
 
168
169
  describe('a foreign DB row on the same path', () => {
169
170
  test('is Unowned, and read works with output undefined', async () => {
170
- fake.stop();
171
171
  fake = startFakeLitellm({
172
172
  masterKey: MASTER_KEY,
173
173
  seed: [{ ...PROPS, id: 'foreign-1', is_from_config: false }],
@@ -18,7 +18,8 @@
18
18
  * ADDRESSED by the id-keyed update/delete calls this provider uses, so it is refused rather than
19
19
  * silently adopted into an id-based lifecycle it cannot actually manage — see docs/litellm.md.
20
20
  * ⛔ NO CREDENTIAL IS A PROP (S25). `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY` are read at call
21
- * time by credentials.ts.
21
+ * time by `@distilled.cloud/litellm`'s own `CredentialsFromEnv` (providers.ts) — the same two
22
+ * variable names the retired hand-rolled `credentials.ts` read.
22
23
  */
23
24
  import { Resource } from 'alchemy';
24
25
  import { Unowned } from 'alchemy/AdoptPolicy';
@@ -28,18 +29,21 @@ import { createPhysicalName } from 'alchemy/PhysicalName';
28
29
  import * as Provider from 'alchemy/Provider';
29
30
  import type { Stack } from 'alchemy/Stack';
30
31
  import type { Stage } from 'alchemy/Stage';
31
- import * as Data from 'effect/Data';
32
+ import type * as misc from '@distilled.cloud/litellm/misc';
32
33
  import * as Effect from 'effect/Effect';
33
34
  import * as Predicate from 'effect/Predicate';
34
35
  import {
35
- type LitellmError,
36
36
  type LitellmRequirements,
37
37
  createPassThroughEndpoint,
38
38
  deletePassThroughEndpoint,
39
39
  listPassThroughEndpoints,
40
40
  updatePassThroughEndpoint,
41
- } from './client.ts';
42
- import type { PassThroughGenericEndpoint } from './generated/pass-through.ts';
41
+ } from './operations.ts';
42
+ import {
43
+ LitellmConfigPathConflictError,
44
+ LitellmLiteralSecretHeaderError,
45
+ LitellmUnaddressableRowError,
46
+ } from './pass-through-errors.ts';
43
47
  import {
44
48
  type PassThroughEndpointAttributes,
45
49
  type PassThroughEndpointProps,
@@ -51,26 +55,12 @@ import {
51
55
  } from './pass-through-form.ts';
52
56
 
53
57
  export type { PassThroughEndpointAttributes, PassThroughEndpointProps };
54
-
55
- export class LitellmConfigPathConflictError extends Data.TaggedError(
56
- 'LitellmConfigPathConflictError',
57
- )<{
58
- readonly path: string;
59
- }> {}
60
- export class LitellmUnaddressableRowError extends Data.TaggedError('LitellmUnaddressableRowError')<{
61
- readonly path: string;
62
- }> {}
63
- export class LitellmLiteralSecretHeaderError extends Data.TaggedError(
64
- 'LitellmLiteralSecretHeaderError',
65
- )<{
66
- readonly headers: readonly string[];
67
- }> {}
68
-
69
- export type PassThroughEndpointError =
70
- | LitellmError
71
- | LitellmConfigPathConflictError
72
- | LitellmUnaddressableRowError
73
- | LitellmLiteralSecretHeaderError;
58
+ export {
59
+ LitellmConfigPathConflictError,
60
+ LitellmLiteralSecretHeaderError,
61
+ LitellmUnaddressableRowError,
62
+ type PassThroughEndpointError,
63
+ } from './pass-through-errors.ts';
74
64
 
75
65
  export interface LiteLLMPassThroughEndpoint extends Resource<
76
66
  'LiteLLM.PassThroughEndpoint',
@@ -88,7 +78,7 @@ export const isLiteLLMPassThroughEndpoint = (value: unknown): value is LiteLLMPa
88
78
  Predicate.hasProperty(value, 'Type') && value.Type === 'LiteLLM.PassThroughEndpoint';
89
79
 
90
80
  const toAttributes = (
91
- row: PassThroughGenericEndpoint,
81
+ row: misc.PassThroughGenericEndpoint,
92
82
  fallbackId: string,
93
83
  ): PassThroughEndpointAttributes => ({
94
84
  ...row,
@@ -104,12 +94,12 @@ const physicalIdOf = (logicalId: string, instanceId: string) =>
104
94
  * file header — or a genuine foreign DB row to hand back `Unowned`).
105
95
  */
106
96
  const locate = (
107
- rows: readonly PassThroughGenericEndpoint[],
97
+ rows: readonly misc.PassThroughGenericEndpoint[],
108
98
  objectId: string,
109
99
  path: string,
110
100
  ): {
111
- readonly mine: PassThroughGenericEndpoint | undefined;
112
- readonly conflict: PassThroughGenericEndpoint | undefined;
101
+ readonly mine: misc.PassThroughGenericEndpoint | undefined;
102
+ readonly conflict: misc.PassThroughGenericEndpoint | undefined;
113
103
  } => {
114
104
  const mine = rows.find((row) => row.id === objectId);
115
105
  const conflict = mine === undefined ? rows.find((row) => row.path === path) : undefined;
@@ -117,13 +107,13 @@ const locate = (
117
107
  };
118
108
 
119
109
  /** The typed refusal for a `path` conflict that cannot ever be adopted (config row, or no id). */
120
- const conflictRefusal = (conflict: PassThroughGenericEndpoint, path: string) =>
110
+ const conflictRefusal = (conflict: misc.PassThroughGenericEndpoint, path: string) =>
121
111
  conflict.is_from_config === true
122
112
  ? new LitellmConfigPathConflictError({ path })
123
113
  : new LitellmUnaddressableRowError({ path });
124
114
 
125
115
  /** `read`'s answer for a path conflict: `Unowned` when it is a real, addressable DB row — a typed refusal otherwise. */
126
- const readConflict = (conflict: PassThroughGenericEndpoint, path: string) =>
116
+ const readConflict = (conflict: misc.PassThroughGenericEndpoint, path: string) =>
127
117
  conflict.is_from_config !== true && conflict.id !== undefined && conflict.id !== null
128
118
  ? Effect.succeed(Unowned(toAttributes(conflict, conflict.id)))
129
119
  : Effect.fail(conflictRefusal(conflict, path));
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The typed refusals `LiteLLM.PassThroughEndpoint` raises on top of `@distilled.cloud/litellm`'s
3
+ * own SDK errors — split out of pass-through-endpoint.ts to keep that file under the house's
4
+ * 250-line cap once this migration added the four named SDK error-union re-exports.
5
+ */
6
+ import type * as misc from '@distilled.cloud/litellm/misc';
7
+ import * as Data from 'effect/Data';
8
+
9
+ export class LitellmConfigPathConflictError extends Data.TaggedError(
10
+ 'LitellmConfigPathConflictError',
11
+ )<{
12
+ readonly path: string;
13
+ }> {}
14
+ export class LitellmUnaddressableRowError extends Data.TaggedError('LitellmUnaddressableRowError')<{
15
+ readonly path: string;
16
+ }> {}
17
+ export class LitellmLiteralSecretHeaderError extends Data.TaggedError(
18
+ 'LitellmLiteralSecretHeaderError',
19
+ )<{
20
+ readonly headers: readonly string[];
21
+ }> {}
22
+
23
+ /**
24
+ * ⛔ THE FOUR SDK ERROR UNIONS, NAMED, NOT `LitellmOpError` ALONE. `catchTag`'s tag-literal
25
+ * inference needs a concrete union to resolve `"BadRequest"`/`"NotFound"` against (the same
26
+ * reason `netbox/resource.ts` leaves `NotFound` folding to each resource file, not its shared
27
+ * engine) — `operations.ts` already returns each call's own precise per-operation error type, so
28
+ * this is just their union, not a re-widening.
29
+ */
30
+ export type PassThroughEndpointError =
31
+ | misc.GetPassThroughEndpointsConfigPassThroughEndpointGetError
32
+ | misc.CreatePassThroughEndpointsConfigPassThroughEndpointPostError
33
+ | misc.UpdatePassThroughEndpointsConfigPassThroughEndpointEndpointIdPostError
34
+ | misc.DeletePassThroughEndpointsConfigPassThroughEndpointDeleteError
35
+ | LitellmConfigPathConflictError
36
+ | LitellmUnaddressableRowError
37
+ | LitellmLiteralSecretHeaderError;
@@ -5,14 +5,17 @@
5
5
  * ★ EXTRACTED SO IT CAN BE TESTED WITHOUT A SERVER — the same reason netbox keeps `prefix-form.ts`
6
6
  * beside `prefix.ts` (S32/testing-and-docs.md).
7
7
  *
8
- * ⛔ PROPS ARE THE GENERATED TYPE, MINUS THE TWO FIELDS THIS RESOURCE OWNS ITSELF: `id` (a
9
- * deterministic physical name — pass-through-endpoint.ts) and `is_from_config` (response-only;
10
- * the vendor sets it, a declaration never does). Deriving `Props` with `Omit<>` rather than
11
- * hand-typing it means a vendor schema change that adds, removes or narrows a field is felt here
12
- * as a compiler error, not silently ignored.
8
+ * ⛔ PROPS ARE `@distilled.cloud/litellm`'s OWN GENERATED TYPE, MINUS THE TWO FIELDS THIS RESOURCE
9
+ * OWNS ITSELF: `id` (a deterministic physical name — pass-through-endpoint.ts) and
10
+ * `is_from_config` (response-only; the vendor sets it, a declaration never does). Deriving
11
+ * `Props` with `Omit<>` rather than hand-typing it means a vendor schema change the SDK's own
12
+ * next regeneration picks up is felt here as a compiler error, not silently ignored — the same
13
+ * reason this used to derive from the kit's own hand-generated `./generated/pass-through.ts`
14
+ * (retired by this migration: the SDK's type is the same shape, generated from the same LiteLLM
15
+ * 1.100.0 OpenAPI document, and keeping a second copy would only drift).
13
16
  */
14
17
  import { deepEqual } from 'alchemy/Diff';
15
- import type * as Generated from './generated/pass-through.ts';
18
+ import type * as Generated from '@distilled.cloud/litellm/misc';
16
19
 
17
20
  export type PassThroughEndpointProps = Omit<
18
21
  Generated.PassThroughGenericEndpoint,
@@ -71,10 +74,17 @@ export const createBody = (
71
74
  * `undefined` OR an explicit `null` here would be dropped by the server anyway; sending neither is
72
75
  * the same outcome with no request made when nothing changed (`Object.keys(body).length === 0`
73
76
  * checked by the caller in pass-through-endpoint.ts).
77
+ *
78
+ * ⛔ `path`/`target` ARE NOT `Partial` HERE, UNLIKE EVERY OTHER FIELD. The SDK's own update
79
+ * request type requires both non-optionally (the vendor's PATCH route still wants the full
80
+ * route/target pair on every call, unlike the other fields' `exclude_none` merge) — typing them
81
+ * as always-present here is what lets `operations.ts`'s `updatePassThroughEndpoint` pass this
82
+ * straight to the SDK without an unsafe cast.
74
83
  */
75
- export const updateBody = (
76
- props: PassThroughEndpointProps,
77
- ): Partial<Generated.PassThroughGenericEndpoint> =>
84
+ export type UpdateBody = Partial<Omit<Generated.PassThroughGenericEndpoint, 'path' | 'target'>> &
85
+ Pick<Generated.PassThroughGenericEndpoint, 'path' | 'target'>;
86
+
87
+ export const updateBody = (props: PassThroughEndpointProps): UpdateBody =>
78
88
  prune({
79
89
  ...settled(props),
80
90
  guardrails: isUnset(props.guardrails) ? undefined : props.guardrails,
@@ -82,7 +92,7 @@ export const updateBody = (
82
92
  path: props.path,
83
93
  target: props.target,
84
94
  timeout: isUnset(props.timeout) ? undefined : props.timeout,
85
- }) as Partial<Generated.PassThroughGenericEndpoint>;
95
+ }) as UpdateBody;
86
96
 
87
97
  /**
88
98
  * Whether ANY of the three nullable fields went from set (in `live`) to unset (in `props`) — the
@@ -3,17 +3,18 @@
3
3
  *
4
4
  * Layer.mergeAll(litellmProviders(), …the stack's other providers)
5
5
  *
6
- * ★ THE DEFAULT READS `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY` AT CALL TIME (credentials.ts),
7
- * never at layer build — pass another `Layer.Layer<LitellmCredentials>` (see
8
- * `litellmCredentialsLayerFor` in a test) to source them another way.
6
+ * ★ THE DEFAULT READS `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY` AT CALL TIME, through
7
+ * `@distilled.cloud/litellm`'s own `CredentialsFromEnv` — the same two variable names the
8
+ * retired hand-rolled `credentials.ts` read — never at layer build. Pass another
9
+ * `Layer.Layer<Credentials>` (see the SDK's own `credentials({ apiKey, baseUrl })` in a test) to
10
+ * source them another way.
9
11
  * ⚠️ `HttpClient` is NOT provided here. It is ambient in every Alchemy runtime the same way it is
10
12
  * for `netboxProviders`-style families in this kit; a stack that already deploys anything over
11
13
  * HTTP already has one.
12
14
  */
15
+ import { type Credentials, CredentialsFromEnv } from '@distilled.cloud/litellm/Credentials';
13
16
  import * as Layer from 'effect/Layer';
14
- import { type LitellmCredentials, litellmCredentialsLayer } from './credentials.ts';
15
17
  import { LiteLLMPassThroughEndpointProvider } from './pass-through-endpoint.ts';
16
18
 
17
- export const litellmProviders = (
18
- creds: Layer.Layer<LitellmCredentials> = litellmCredentialsLayer,
19
- ) => LiteLLMPassThroughEndpointProvider().pipe(Layer.provide(creds));
19
+ export const litellmProviders = (creds: Layer.Layer<Credentials> = CredentialsFromEnv) =>
20
+ LiteLLMPassThroughEndpointProvider().pipe(Layer.provide(creds));