@homeflare/alchemy 0.37.3 → 0.37.5

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.
@@ -34,6 +34,7 @@ import type { LitellmOpContext } from '@distilled.cloud/litellm/Protocol';
34
34
  import type { ConfigError } from '@distilled.cloud/litellm/Errors';
35
35
  import * as Effect from 'effect/Effect';
36
36
  import * as Semaphore from 'effect/Semaphore';
37
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
37
38
 
38
39
  export type { LitellmOpContext } from '@distilled.cloud/litellm/Protocol';
39
40
  export type LitellmRequirements = LitellmOpContext;
@@ -67,6 +68,18 @@ const semaphoreFor = (baseUrl: string): Effect.Effect<Semaphore.Semaphore> =>
67
68
  * `Credentials`'s own type — so `E` (the wrapped operation's error) already contains it, and
68
69
  * this widening is a genuine no-op for callers, not dead type-state left over from the revert.
69
70
  */
71
+ /**
72
+ * ⛔ THE STACK'S AMBIENT `HttpClient` MAY BE CADDY'S. `caddyProviders()` provideMerges a client
73
+ * that dials the admin listener and ignores the request host (`caddy-http-client.ts`). Measured
74
+ * 2026-09-25: that client answers `GET /config/pass_through_endpoint` with HTTP 200 `null`
75
+ * (Caddy's empty-config body). The decoded value then has no `endpoints`, and `locate` throws
76
+ * `rows.find` of undefined. FetchHttpClient uses the URL the LiteLLM protocol built. An outer
77
+ * `Fetch` service still wins inside that client, which is how the fake proxy stays the test
78
+ * transport.
79
+ */
80
+ const throughFetch = <A, E, R>(io: Effect.Effect<A, E, R>) =>
81
+ io.pipe(Effect.provide(FetchHttpClient.layer));
82
+
70
83
  const mutate = <A, E>(
71
84
  io: Effect.Effect<A, E, LitellmOpContext>,
72
85
  ): Effect.Effect<A, E | ConfigError, LitellmOpContext> =>
@@ -74,18 +87,32 @@ const mutate = <A, E>(
74
87
  const resolve = yield* Credentials;
75
88
  const creds = yield* resolve;
76
89
  const semaphore = yield* semaphoreFor(creds.apiBaseUrl);
77
- return yield* semaphore.withPermits(1)(io);
90
+ return yield* semaphore.withPermits(1)(throughFetch(io));
78
91
  });
79
92
 
93
+ const endpointsOf = (
94
+ response: misc.PassThroughEndpointResponse,
95
+ ): Effect.Effect<readonly misc.PassThroughGenericEndpoint[]> => {
96
+ const rows = response.endpoints;
97
+ if (!Array.isArray(rows)) {
98
+ return Effect.die(
99
+ new Error(
100
+ 'GET /config/pass_through_endpoint decoded no endpoints array. The call reached something other than the LiteLLM proxy.',
101
+ ),
102
+ );
103
+ }
104
+ return Effect.succeed(rows);
105
+ };
106
+
80
107
  /** GET /config/pass_through_endpoint — every row, config-file and DB alike. Not under the semaphore: reads don't race the field. */
81
108
  export const listPassThroughEndpoints = (): Effect.Effect<
82
109
  readonly misc.PassThroughGenericEndpoint[],
83
110
  misc.GetPassThroughEndpointsConfigPassThroughEndpointGetError,
84
111
  LitellmOpContext
85
112
  > =>
86
- misc
87
- .getPassThroughEndpointsConfigPassThroughEndpointGet({})
88
- .pipe(Effect.map((response) => response.endpoints));
113
+ throughFetch(misc.getPassThroughEndpointsConfigPassThroughEndpointGet({})).pipe(
114
+ Effect.flatMap(endpointsOf),
115
+ );
89
116
 
90
117
  /** POST /config/pass_through_endpoint. `body` carries `id` — see pass-through-form.ts. */
91
118
  export const createPassThroughEndpoint = (
@@ -20,6 +20,10 @@
20
20
  * ⛔ NO CREDENTIAL IS A PROP (S25). `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY` are read at call
21
21
  * time by `@distilled.cloud/litellm`'s own `CredentialsFromEnv` (providers.ts) — the same two
22
22
  * variable names the retired hand-rolled `credentials.ts` read.
23
+ * ⛔ CREDENTIALS ARE NOT A RESOURCE REQUIREMENT. `LitellmOpContext` (`Credentials | HttpClient`)
24
+ * belongs to the handlers at Apply, on `litellmProviders`'s layer. The 5th `Resource` parameter
25
+ * used to be `LitellmRequirements | Stack | Stage`, so a stack body required `Credentials`, and
26
+ * `Alchemy.Stack` rejects that (`"LitellmCredentials"` is not `"Stack"`). Measured 2026-09-24.
23
27
  */
24
28
  import { Resource } from 'alchemy';
25
29
  import { Unowned } from 'alchemy/AdoptPolicy';
@@ -27,13 +31,10 @@ import { isResolved } from 'alchemy/Diff';
27
31
  import type { Input } from 'alchemy/Input';
28
32
  import { createPhysicalName } from 'alchemy/PhysicalName';
29
33
  import * as Provider from 'alchemy/Provider';
30
- import type { Stack } from 'alchemy/Stack';
31
- import type { Stage } from 'alchemy/Stage';
32
34
  import type * as misc from '@distilled.cloud/litellm/misc';
33
35
  import * as Effect from 'effect/Effect';
34
36
  import * as Predicate from 'effect/Predicate';
35
37
  import {
36
- type LitellmRequirements,
37
38
  createPassThroughEndpoint,
38
39
  deletePassThroughEndpoint,
39
40
  listPassThroughEndpoints,
@@ -65,9 +66,7 @@ export {
65
66
  export interface LiteLLMPassThroughEndpoint extends Resource<
66
67
  'LiteLLM.PassThroughEndpoint',
67
68
  PassThroughEndpointProps,
68
- PassThroughEndpointAttributes,
69
- never,
70
- LitellmRequirements | Stack | Stage
69
+ PassThroughEndpointAttributes
71
70
  > {}
72
71
 
73
72
  export const LiteLLMPassThroughEndpoint = Resource<LiteLLMPassThroughEndpoint>(
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A stack that also provides Caddy's admin `HttpClient` must not send LiteLLM's list there.
3
+ *
4
+ * ⛔ MEASURED 2026-09-25. `caddyProviders()` provideMerges a client that dials `127.0.0.1:2019`
5
+ * and ignores the request host. `GET /config/pass_through_endpoint` on that listener is HTTP
6
+ * 200 `null`. `listPassThroughEndpoints` then has no `endpoints` array and `locate` throws.
7
+ * The call provides `FetchHttpClient`, which still reads an outer `Fetch` — this fake.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import { credentials } from '@distilled.cloud/litellm/Credentials';
11
+ import * as Effect from 'effect/Effect';
12
+ import * as Layer from 'effect/Layer';
13
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
14
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
15
+ import { FAKE_BASE, startFakeLitellm } from './fake-litellm.ts';
16
+ import { listPassThroughEndpoints } from './operations.ts';
17
+
18
+ const MASTER_KEY = 'sk-test-master';
19
+
20
+ describe('the proxy list', () => {
21
+ test('uses Fetch, not an ambient client that answers null', async () => {
22
+ const fake = startFakeLitellm({
23
+ masterKey: MASTER_KEY,
24
+ seed: [{ auth: true, id: 'live-1', path: '/bria', target: 'https://api.bria.ai' }],
25
+ });
26
+ const ambient = HttpClient.make(() => Effect.die(new Error('ambient client')));
27
+ const rows = await Effect.runPromise(
28
+ listPassThroughEndpoints().pipe(
29
+ Effect.provideService(HttpClient.HttpClient, ambient),
30
+ Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fake.fetch)),
31
+ Effect.provide(credentials({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE })),
32
+ Effect.provide(FetchHttpClient.layer),
33
+ ) as Effect.Effect<readonly { id?: string | null }[], unknown, never>,
34
+ );
35
+ expect(rows.map((row) => row.id)).toEqual(['live-1']);
36
+ expect(fake.requests().map((request) => request.path)).toEqual([
37
+ '/config/pass_through_endpoint',
38
+ ]);
39
+ });
40
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A real `Alchemy.Stack` whose body `yield*`s `LiteLLM.PassThroughEndpoint`, with
3
+ * `litellmProviders` as the providers layer.
4
+ *
5
+ * ⛔ THE TYPE IS THE BUG. The resource used to put `LitellmRequirements`
6
+ * (`Credentials | HttpClient`) on its 5th type parameter, so this construction failed
7
+ * TS2345: `Credentials` is not `ProviderServices | StackServices`
8
+ * (`"LitellmCredentials"` is not `"Stack"`). No cast. `tsc` fails again if that leak
9
+ * returns. The stack value is not run — `Alchemy.Stack` returns a lazy effect
10
+ * (github/repo-policy-stack.test.ts measured the same).
11
+ *
12
+ * ★ THE DEPLOY IS THE OTHER HALF. `fakeStack` runs Alchemy's own Plan and Apply over the
13
+ * fake proxy, and its providers layer is `litellmProviders` (`fake-stack.ts`). A sealed
14
+ * `Layer.provide` builds the provider and then hides `Credentials`, so `reconcile` dies
15
+ * with Service not found before any request. A live row means the handler ran with the
16
+ * key present.
17
+ */
18
+ import { expect, test } from 'bun:test';
19
+ import { credentials } from '@distilled.cloud/litellm/Credentials';
20
+ import * as Alchemy from 'alchemy';
21
+ import * as Effect from 'effect/Effect';
22
+ import { FAKE_BASE, startFakeLitellm } from './fake-litellm.ts';
23
+ import { fakeStack } from './fake-stack.ts';
24
+ import { LiteLLMPassThroughEndpoint } from './pass-through-endpoint.ts';
25
+ import { litellmProviders } from './providers.ts';
26
+
27
+ const MASTER_KEY = 'sk-test-master';
28
+ const props = { path: '/bria', target: 'https://api.bria.ai' };
29
+
30
+ test('an Alchemy.Stack body can yield LiteLLMPassThroughEndpoint, and apply reaches the proxy', async () => {
31
+ const stack = Alchemy.Stack(
32
+ 'litellm-passthrough',
33
+ {
34
+ providers: litellmProviders(credentials({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE })),
35
+ state: Alchemy.inMemoryState(),
36
+ },
37
+ Effect.gen(function* () {
38
+ return yield* LiteLLMPassThroughEndpoint('Endpoint', props);
39
+ }),
40
+ );
41
+ // Not run — see the file header. `tsc` is the assertion on this value.
42
+ expect(stack).toBeDefined();
43
+
44
+ const fake = startFakeLitellm({ masterKey: MASTER_KEY });
45
+ const planned = await fakeStack({ apiKey: MASTER_KEY, baseUrl: FAKE_BASE }, fake.fetch).deploy(
46
+ Effect.gen(function* () {
47
+ yield* LiteLLMPassThroughEndpoint('Endpoint', props);
48
+ }),
49
+ );
50
+
51
+ expect(planned).toEqual({ Endpoint: 'create' });
52
+ expect(fake.rows().some((row) => row.path === '/bria')).toBe(true);
53
+ expect(fake.requests().some((call) => call.method === 'POST')).toBe(true);
54
+ });
@@ -10,11 +10,17 @@
10
10
  * source them another way.
11
11
  * ⚠️ `HttpClient` is NOT provided here. It is ambient in every Alchemy runtime the same way it is
12
12
  * for `netboxProviders`-style families in this kit; a stack that already deploys anything over
13
- * HTTP already has one.
13
+ * HTTP already has one. Do not add a second client to this layer.
14
+ * ⚠️ `Layer.provideMerge`, NEVER PLAIN `Layer.provide`, for the credentials layer. The handlers
15
+ * still need `Credentials` when the engine calls `read`/`diff`/`reconcile`. `Provider.effect`
16
+ * puts that need on the layer. Plain `provide` satisfies it only while the `Provider` value is
17
+ * built, then hides `Credentials` from those later calls. Measured on Caddy, 2026-09-23:
18
+ * `caddy/providers.ts`. `provideMerge` feeds credentials in and keeps them in the layer the
19
+ * engine runs the handlers with.
14
20
  */
15
21
  import { type Credentials, CredentialsFromEnv } from '@distilled.cloud/litellm/Credentials';
16
22
  import * as Layer from 'effect/Layer';
17
23
  import { LiteLLMPassThroughEndpointProvider } from './pass-through-endpoint.ts';
18
24
 
19
25
  export const litellmProviders = (creds: Layer.Layer<Credentials> = CredentialsFromEnv) =>
20
- LiteLLMPassThroughEndpointProvider().pipe(Layer.provide(creds));
26
+ LiteLLMPassThroughEndpointProvider().pipe(Layer.provideMerge(creds));
@@ -40,7 +40,11 @@ export const extractMembers = async (
40
40
  // a peak RSS of 1116 MB for vmalert alone under bun 1.4.0. `pull` hands over the next slice only
41
41
  // when the stream asks, so backpressure bounds what is inflated ahead of the tar reader.
42
42
  let offset = 0;
43
- const source = new ReadableStream<Uint8Array>({
43
+ // ★ The writable side of DecompressionStream accepts BufferSource (ArrayBuffer or a
44
+ // view). pipeThrough is invariant there, so a Uint8Array source is rejected even
45
+ // though every slice enqueued is one. Measured as TS2345 under TypeScript 7. No
46
+ // zlib package: Bun and Node both ship this stream.
47
+ const source = new ReadableStream<ArrayBufferView | ArrayBuffer>({
44
48
  pull(controller) {
45
49
  if (offset >= gzipped.length) {
46
50
  controller.close();