@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.
- package/dist/linux/directory-lifecycle.d.ts.map +1 -1
- package/dist/linux/directory-read.d.ts +14 -0
- package/dist/linux/directory-read.d.ts.map +1 -0
- package/dist/linux/directory.d.ts +5 -3
- package/dist/linux/directory.d.ts.map +1 -1
- package/dist/linux/index.js +25 -8
- package/dist/linux/index.js.map +6 -5
- package/dist/litellm/fake-stack.d.ts +7 -7
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/index.js +13 -4
- package/dist/litellm/index.js.map +5 -5
- package/dist/litellm/operations.d.ts.map +1 -1
- package/dist/litellm/pass-through-endpoint.d.ts +9 -8
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +8 -2
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/release/archive.d.ts.map +1 -1
- package/dist/release/index.js.map +2 -2
- package/package.json +1 -1
- package/src/linux/directory-lifecycle.ts +14 -2
- package/src/linux/directory-read.ts +24 -0
- package/src/linux/directory.test.ts +44 -0
- package/src/linux/directory.ts +26 -14
- package/src/litellm/fake-stack.ts +13 -10
- package/src/litellm/operations.ts +31 -4
- package/src/litellm/pass-through-endpoint.ts +5 -6
- package/src/litellm/pass-through-fetch-client.test.ts +40 -0
- package/src/litellm/pass-through-stack.test.ts +54 -0
- package/src/litellm/providers.ts +8 -2
- package/src/release/archive.ts +5 -1
|
@@ -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
|
-
.
|
|
88
|
-
|
|
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
|
+
});
|
package/src/litellm/providers.ts
CHANGED
|
@@ -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.
|
|
26
|
+
LiteLLMPassThroughEndpointProvider().pipe(Layer.provideMerge(creds));
|
package/src/release/archive.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|