@xandreed/plugin-models 0.13.0 → 0.15.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/README.md +1 -1
- package/dist/auth/anthropicOAuth.d.ts +45 -11
- package/dist/auth/anthropicOAuth.js +23 -23
- package/dist/auth/auth-store.conformance.d.ts +63 -0
- package/dist/auth/auth-store.conformance.js +152 -0
- package/dist/auth/credential-input.entity.d.ts +52 -0
- package/dist/auth/credential-input.entity.functions.d.ts +18 -0
- package/dist/auth/credential-input.entity.functions.js +12 -0
- package/dist/auth/credential-input.entity.js +22 -0
- package/dist/auth/credential-refresh.entity.d.ts +17 -0
- package/dist/auth/credential-refresh.entity.functions.d.ts +57 -0
- package/dist/auth/credential-refresh.entity.functions.js +21 -0
- package/dist/auth/credential-refresh.entity.js +2 -0
- package/dist/auth/localAuth.d.ts +12 -2
- package/dist/auth/localAuth.js +58 -86
- package/dist/auth/memory-auth.adapter.d.ts +35 -0
- package/dist/auth/memory-auth.adapter.js +35 -0
- package/dist/auth/openAiCodexOAuth.d.ts +28 -6
- package/dist/auth/openAiCodexOAuth.js +22 -32
- package/dist/auth/token-response.entity.d.ts +21 -0
- package/dist/auth/token-response.entity.functions.d.ts +20 -0
- package/dist/auth/token-response.entity.functions.js +10 -0
- package/dist/auth/token-response.entity.js +10 -0
- package/dist/compat/index.d.ts +2 -0
- package/dist/compat/index.js +1 -0
- package/dist/conformance/index.d.ts +6 -0
- package/dist/conformance/index.js +3 -0
- package/dist/index.d.ts +30 -13
- package/dist/index.js +25 -50
- package/dist/llm/chat-wire.entity.d.ts +123 -0
- package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
- package/dist/llm/chat-wire.entity.functions.js +10 -0
- package/dist/llm/chat-wire.entity.js +37 -0
- package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
- package/dist/llm/compat-telemetry.adapter.js +21 -0
- package/dist/llm/compat.d.ts +84 -33
- package/dist/llm/compat.js +188 -121
- package/dist/llm/model-completion.adapter.d.ts +49 -0
- package/dist/llm/model-completion.adapter.js +77 -0
- package/dist/llm/model-completion.entity.d.ts +4 -0
- package/dist/llm/model-completion.entity.functions.d.ts +3 -0
- package/dist/llm/model-completion.entity.functions.js +7 -0
- package/dist/llm/model-completion.entity.js +7 -0
- package/dist/llm/model-retry-policy.entity.d.ts +37 -0
- package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
- package/dist/llm/model-retry-policy.entity.functions.js +33 -0
- package/dist/llm/model-retry-policy.entity.js +23 -0
- package/dist/llm/model-settings.entity.d.ts +85 -0
- package/dist/llm/model-settings.entity.functions.d.ts +11 -0
- package/dist/llm/model-settings.entity.functions.js +49 -0
- package/dist/llm/model-settings.entity.js +28 -0
- package/dist/llm/model-usage.entity.d.ts +61 -0
- package/dist/llm/model-usage.entity.functions.d.ts +11 -0
- package/dist/llm/model-usage.entity.functions.js +24 -0
- package/dist/llm/model-usage.entity.js +19 -0
- package/dist/llm/modelCatalog.d.ts +47 -7
- package/dist/llm/modelCatalog.js +31 -57
- package/dist/llm/openAiCodex.d.ts +18 -7
- package/dist/llm/openAiCodex.js +57 -45
- package/dist/llm/openAiCodexWebSocket.d.ts +60 -7
- package/dist/llm/openAiCodexWebSocket.js +113 -105
- package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
- package/dist/llm/prompt-cache-frame.entity.js +10 -0
- package/dist/llm/prompt-cache.entity.d.ts +57 -0
- package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
- package/dist/llm/prompt-cache.entity.functions.js +73 -0
- package/dist/llm/prompt-cache.entity.js +27 -0
- package/dist/llm/provider-client.adapter.d.ts +9 -0
- package/dist/llm/provider-client.adapter.js +60 -0
- package/dist/llm/provider-failure.entity.d.ts +42 -0
- package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
- package/dist/llm/provider-failure.entity.functions.js +67 -0
- package/dist/llm/provider-failure.entity.js +20 -0
- package/dist/llm/provider-profile.entity.d.ts +79 -0
- package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
- package/dist/llm/provider-profile.entity.functions.js +83 -0
- package/dist/llm/provider-profile.entity.js +10 -0
- package/dist/llm/providers.d.ts +125 -29
- package/dist/llm/providers.js +311 -117
- package/dist/llm/retry.d.ts +11 -47
- package/dist/llm/retry.js +33 -52
- package/dist/llm/router.d.ts +56 -28
- package/dist/llm/router.js +125 -88
- package/dist/llm/sse.d.ts +58 -29
- package/dist/llm/sse.js +47 -39
- package/dist/llm/thinking-binding.entity.d.ts +49 -0
- package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
- package/dist/llm/thinking-binding.entity.functions.js +16 -0
- package/dist/llm/thinking-binding.entity.js +11 -0
- package/dist/llm/utilityLlm.d.ts +14 -5
- package/dist/llm/utilityLlm.js +18 -13
- package/dist/plugin.adapter.d.ts +154 -6
- package/dist/plugin.adapter.js +53 -10
- package/dist/ports/auth-store.port.d.ts +73 -0
- package/dist/ports/auth-store.port.js +24 -0
- package/dist/ports/host-credentials.port.d.ts +22 -0
- package/dist/ports/host-credentials.port.js +4 -0
- package/dist/ports/model-call-completion.port.d.ts +9 -0
- package/dist/ports/model-call-completion.port.js +2 -0
- package/dist/ports/model-endpoints.port.d.ts +66 -19
- package/dist/ports/model-endpoints.port.js +22 -4
- package/dist/ports/model-retry-policy.port.d.ts +17 -0
- package/dist/ports/model-retry-policy.port.js +3 -0
- package/dist/ports/model-settings.port.d.ts +32 -0
- package/dist/ports/model-settings.port.js +4 -0
- package/dist/ports/models-transport.port.d.ts +53 -0
- package/dist/ports/models-transport.port.js +7 -0
- package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
- package/dist/ports/prompt-cache-frame.port.js +4 -0
- package/dist/ports/settings-store.port.d.ts +70 -0
- package/dist/ports/settings-store.port.js +33 -0
- package/dist/ports/thinking-binding.port.d.ts +7 -0
- package/dist/ports/thinking-binding.port.js +4 -0
- package/dist/settings/localSettings.d.ts +2 -1
- package/dist/settings/localSettings.js +18 -61
- package/dist/settings/memory-settings.adapter.d.ts +12 -0
- package/dist/settings/memory-settings.adapter.js +11 -0
- package/dist/settings/roleView.d.ts +3 -1
- package/dist/settings/roleView.js +1 -1
- package/dist/settings/settings-store.conformance.d.ts +47 -0
- package/dist/settings/settings-store.conformance.js +100 -0
- package/dist/settings/settings-value.entity.d.ts +15 -0
- package/dist/settings/settings-value.entity.functions.d.ts +51 -0
- package/dist/settings/settings-value.entity.functions.js +42 -0
- package/dist/settings/settings-value.entity.js +2 -0
- package/dist/storage/json-file.d.ts +18 -0
- package/dist/storage/json-file.js +26 -0
- package/dist/transport/http-proxy.entity.d.ts +104 -0
- package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
- package/dist/transport/http-proxy.entity.functions.js +112 -0
- package/dist/transport/http-proxy.entity.js +42 -0
- package/dist/transport/model-request.conformance.d.ts +13 -0
- package/dist/transport/model-request.conformance.js +36 -0
- package/dist/transport/models-transport.adapter.d.ts +49 -0
- package/dist/transport/models-transport.adapter.js +99 -0
- package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
- package/dist/transport/resilient-http-client.adapter.js +177 -0
- package/dist/transport/undici-fetch.adapter.d.ts +13 -0
- package/dist/transport/undici-fetch.adapter.js +1 -0
- package/package.json +13 -6
- package/dist/auth/anthropicOAuth.test.d.ts +0 -1
- package/dist/auth/localAuth.test.d.ts +0 -1
- package/dist/llm/compat.test.d.ts +0 -1
- package/dist/llm/modelCatalog.test.d.ts +0 -1
- package/dist/llm/providers.test.d.ts +0 -1
- package/dist/llm/retry.test.d.ts +0 -1
- package/dist/llm/router.test.d.ts +0 -1
- package/dist/llm/sse.test.d.ts +0 -1
- package/dist/plugin.adapter.test.d.ts +0 -1
- package/dist/settings/localSettings.test.d.ts +0 -1
- package/dist/settings/roleView.test.d.ts +0 -1
|
@@ -1,72 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { join } from "node:path";
|
|
3
2
|
import { Effect, Layer, Option } from "effect";
|
|
4
|
-
import {
|
|
3
|
+
import { SettingsError, SettingsStore } from "../ports/settings-store.port.js";
|
|
4
|
+
import { readJsonObject, withFileLock, writeJsonObject } from "../storage/json-file.js";
|
|
5
|
+
import { coerceSettingValue, ROLE_FIELDS, roleSelection, settingsFromRecord } from "./settings-value.entity.functions.js";
|
|
5
6
|
const configPaths = (cwd, home) => [
|
|
6
7
|
join(home, ".efferent", "config.json"),
|
|
7
8
|
join(cwd, ".efferent", "config.json")
|
|
8
|
-
], readConfig = (path) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
], readConfig = (path) => readJsonObject(path, (reason) => new SettingsError({
|
|
10
|
+
code: "settings.corrupt",
|
|
11
|
+
message: `${path} ${reason}: fix or remove it; nothing was read from it or written to it`
|
|
12
|
+
})).pipe(Effect.map(Option.getOrElse(() => ({})))), writeKey = (cwd, key, value) => {
|
|
13
|
+
const path = join(cwd, ".efferent", "config.json");
|
|
14
|
+
return withFileLock(path)(Effect.gen(function* () {
|
|
15
|
+
const current = yield* readConfig(path), next = Option.match(value, {
|
|
16
|
+
onNone: () => Object.fromEntries(Object.entries(current).filter(([k]) => k !== key)),
|
|
17
|
+
onSome: (v) => ({ ...current, [key]: v })
|
|
15
18
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (raw === "false" || raw === "off")
|
|
20
|
-
return Effect.succeed(!1);
|
|
21
|
-
return Effect.fail(new SettingsError({ message: `${key} must be true/false \u2014 got "${raw}"` }));
|
|
22
|
-
}
|
|
23
|
-
if (key === "reasoningEffort" || key === "codeReasoningEffort" || key === "fastReasoningEffort")
|
|
24
|
-
return Option.match(asReasoningEffort(raw), {
|
|
25
|
-
onNone: () => Effect.fail(new SettingsError({ message: `reasoningEffort is not supported: "${raw}"` })),
|
|
26
|
-
onSome: Effect.succeed
|
|
27
|
-
});
|
|
28
|
-
const parsed = Number(raw);
|
|
29
|
-
return Number.isInteger(parsed) && parsed >= 1 ? Effect.succeed(parsed) : Effect.fail(new SettingsError({ message: `${key} must be a positive integer \u2014 got "${raw}"` }));
|
|
30
|
-
}, writeKey = (cwd, key, value) => Effect.gen(function* () {
|
|
31
|
-
const path = join(cwd, ".efferent", "config.json"), current = yield* readConfig(path), next = Option.match(value, {
|
|
32
|
-
onNone: () => Object.fromEntries(Object.entries(current).filter(([k]) => k !== key)),
|
|
33
|
-
onSome: (v) => ({ ...current, [key]: v })
|
|
34
|
-
});
|
|
35
|
-
yield* writeConfigAtomic(path, next);
|
|
36
|
-
});
|
|
19
|
+
yield* writeJsonObject(path, next, Option.none(), (reason) => new SettingsError({ message: `config.json write failed: ${reason}` }));
|
|
20
|
+
}));
|
|
21
|
+
};
|
|
37
22
|
export const LocalSettingsStoreLive = (cwd, home) => Layer.succeed(SettingsStore, {
|
|
38
|
-
load: Effect.
|
|
39
|
-
|
|
40
|
-
return new EngineSettings({
|
|
41
|
-
model: asString(merged.model),
|
|
42
|
-
codeModel: asString(merged.codeModel),
|
|
43
|
-
fastModel: asString(merged.fastModel),
|
|
44
|
-
fallbackModel: asString(merged.fallbackModel),
|
|
45
|
-
sandbox: asBoolean(merged.sandbox),
|
|
46
|
-
viMode: asBoolean(merged.viMode),
|
|
47
|
-
maxAttempts: asNumber(merged.maxAttempts),
|
|
48
|
-
budgetMillis: asNumber(merged.budgetMillis),
|
|
49
|
-
reasoningEffort: Option.orElse(asReasoningEffort(merged.reasoningEffort), () => asReasoningEffort(merged.openAiReasoningEffort)),
|
|
50
|
-
codeReasoningEffort: asReasoningEffort(merged.codeReasoningEffort),
|
|
51
|
-
fastReasoningEffort: asReasoningEffort(merged.fastReasoningEffort)
|
|
52
|
-
});
|
|
53
|
-
}),
|
|
54
|
-
setRole: (role, selection) => writeKey(cwd, ROLE_KEYS[role], selection),
|
|
23
|
+
load: Effect.forEach(configPaths(cwd, home), readConfig, { concurrency: 1 }).pipe(Effect.map((configs) => settingsFromRecord(configs.reduce((acc, c) => ({ ...acc, ...c }), {})))),
|
|
24
|
+
setRole: (role, selection) => roleSelection(role, selection).pipe(Effect.flatMap((checked) => writeKey(cwd, ROLE_FIELDS[role], checked))),
|
|
55
25
|
set: (key, value) => Option.match(value, {
|
|
56
26
|
onNone: () => writeKey(cwd, key, Option.none()),
|
|
57
27
|
onSome: (raw) => coerceSettingValue(key, raw).pipe(Effect.flatMap((coerced) => writeKey(cwd, key, Option.some(coerced))))
|
|
58
28
|
})
|
|
59
29
|
});
|
|
60
|
-
const ROLE_KEYS = {
|
|
61
|
-
general: "model",
|
|
62
|
-
code: "codeModel",
|
|
63
|
-
fast: "fastModel"
|
|
64
|
-
}, writeConfigAtomic = (path, config) => Effect.tryPromise({
|
|
65
|
-
try: async () => {
|
|
66
|
-
await mkdir(dirname(path), { recursive: !0 });
|
|
67
|
-
const tmp = `${path}.tmp-${process.pid}`;
|
|
68
|
-
await writeFile(tmp, JSON.stringify(config, null, 2), "utf-8");
|
|
69
|
-
await rename(tmp, path);
|
|
70
|
-
},
|
|
71
|
-
catch: (e) => new SettingsError({ message: `config.json write failed: ${String(e)}` })
|
|
72
|
-
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { EngineSettings, SettingsStore } from "../ports/settings-store.port.js";
|
|
3
|
+
/**
|
|
4
|
+
* `MemorySettingsStoreLive(initial)` — a SettingsStore that never touches
|
|
5
|
+
* disk: one writable value for the layer's lifetime, starting at `initial`.
|
|
6
|
+
* `setRole` and `set` check their value the way the file store does
|
|
7
|
+
* (a `"<provider>:<modelId>"` selection; each key's string form) and refuse
|
|
8
|
+
* one that does not parse, leaving the settings as they were.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare const MemorySettingsStoreLive: (initial?: EngineSettings) => Layer.Layer<SettingsStore>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect, Layer, SynchronizedRef } from "effect";
|
|
2
|
+
import { EngineSettings, SettingsStore } from "../ports/settings-store.port.js";
|
|
3
|
+
import { applyRole, applySetting } from "./settings-value.entity.functions.js";
|
|
4
|
+
export const MemorySettingsStoreLive = (initial = new EngineSettings({})) => Layer.effect(SettingsStore, Effect.gen(function* () {
|
|
5
|
+
const state = yield* SynchronizedRef.make(initial);
|
|
6
|
+
return SettingsStore.of({
|
|
7
|
+
load: SynchronizedRef.get(state),
|
|
8
|
+
setRole: (role, selection) => SynchronizedRef.updateEffect(state, (settings) => applyRole(settings, role, selection)),
|
|
9
|
+
set: (key, value) => SynchronizedRef.updateEffect(state, (settings) => applySetting(settings, key, value))
|
|
10
|
+
});
|
|
11
|
+
}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
|
-
import { SettingsStore } from "
|
|
2
|
+
import { SettingsStore } from "../ports/settings-store.port.js";
|
|
3
3
|
/**
|
|
4
4
|
* A role-scoped VIEW of the settings store: `load` re-maps the general
|
|
5
5
|
* `model` to the role's selection (falling back to `model` when the role is
|
|
@@ -7,5 +7,7 @@ import { SettingsStore } from "@xandreed/core";
|
|
|
7
7
|
* LanguageModel router above all — runs on that role WITHOUT knowing roles
|
|
8
8
|
* exist. Compose it under `LanguageModelLive` for the scope that should use
|
|
9
9
|
* the role (e.g. the forge implementor on "code"); writes pass through.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
10
12
|
*/
|
|
11
13
|
export declare const roleModelView: (role: "code" | "fast") => Layer.Layer<SettingsStore, never, SettingsStore>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Layer, Option } from "effect";
|
|
2
|
-
import { EngineSettings, SettingsStore } from "
|
|
2
|
+
import { EngineSettings, SettingsStore } from "../ports/settings-store.port.js";
|
|
3
3
|
export const roleModelView = (role) => Layer.effect(SettingsStore, Effect.map(SettingsStore, (inner) => ({
|
|
4
4
|
load: Effect.map(inner.load, (s) => new EngineSettings({
|
|
5
5
|
...s,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Context, Scope } from "effect";
|
|
3
|
+
import type { ConformanceCheck } from "@xandreed/core/conformance";
|
|
4
|
+
import type { SettingsStore } from "../ports/settings-store.port.js";
|
|
5
|
+
type Store = Context.Service.Shape<typeof SettingsStore>;
|
|
6
|
+
/**
|
|
7
|
+
* What the kit is told about a store, and can do to it beyond calling it.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export interface SettingsStoreConformanceOptions {
|
|
12
|
+
/** Whether `setRole` and `set` write, or every write is refused. */
|
|
13
|
+
readonly writable: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Damage the store's backing data (a file that is not JSON) for the
|
|
16
|
+
* scope's lifetime, and repair it when the scope closes: the corrupt check.
|
|
17
|
+
* Damage what the store writes to, so a write that ignores the damage shows.
|
|
18
|
+
* A store with nothing to damage, such as one in memory, leaves it out.
|
|
19
|
+
*/
|
|
20
|
+
readonly corrupt?: Effect.Effect<void, never, Scope.Scope>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The SettingsStore contract. Every store: `load` is stable. A writable
|
|
24
|
+
* store: each role reads back what `setRole` wrote and clearing it returns
|
|
25
|
+
* the value before the set; `set` accepts each key's canonical string form
|
|
26
|
+
* and `load` gives it typed; `None` clears a key back to the value before the
|
|
27
|
+
* set; a value that does not parse (a selection that is not
|
|
28
|
+
* `"<provider>:<modelId>"`, a count that is not a whole number of at least
|
|
29
|
+
* 1, an unknown effort) fails with a `SettingsError` and changes
|
|
30
|
+
* nothing, through `set` and `setRole` alike. A read-only store refuses
|
|
31
|
+
* every write with a `SettingsError` and changes nothing.
|
|
32
|
+
*
|
|
33
|
+
* The kit writes the store's shared keys (settings have no per-check ids),
|
|
34
|
+
* and restores each key it touches; do not run it against settings that are
|
|
35
|
+
* in use. A key it would touch that holds a value the store must refuse (a
|
|
36
|
+
* selection that is not `"<provider>:<modelId>"`, a count below 1, left by
|
|
37
|
+
* an older, unchecked write) could not be put back, so the check that would
|
|
38
|
+
* touch it fails before writing anything.
|
|
39
|
+
*
|
|
40
|
+
* With `options.corrupt`, a store whose backing data cannot be read fails
|
|
41
|
+
* `load` (and, writable, `setRole` and `set`) with a `SettingsError` of code
|
|
42
|
+
* `settings.corrupt` instead of reading it as defaults.
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export declare const settingsStoreConformance: (store: Store, options: SettingsStoreConformanceOptions) => ReadonlyArray<ConformanceCheck>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { canonicalJson, harnessCodeOf, parseModelSelection } from "@xandreed/core";
|
|
3
|
+
import { check, ConformanceFailure } from "@xandreed/core/conformance";
|
|
4
|
+
import { randomUuid } from "@xandreed/core/internal";
|
|
5
|
+
import { SETTINGS_KEYS } from "../ports/settings-store.port.js";
|
|
6
|
+
const fail = (id, message) => Effect.fail(new ConformanceFailure({ check: id, message })), expect = (id, holds, message) => holds ? Effect.void : fail(id, message), ROLE_FIELDS = { general: "model", code: "codeModel", fast: "fastModel" }, ROLES = ["general", "code", "fast"], FIELDS = ["model", "codeModel", "fastModel", ...SETTINGS_KEYS], fieldOf = (settings, field) => settings[field], valueOf = (settings, field) => Option.map(fieldOf(settings, field), String), snapshot = (settings) => canonicalJson(Object.fromEntries(FIELDS.map((field) => [field, Option.getOrNull(valueOf(settings, field))]))), others = (settings, except) => canonicalJson(Object.fromEntries(FIELDS.filter((field) => field !== except).map((field) => [field, Option.getOrNull(valueOf(settings, field))]))), selection = (label) => Effect.map(randomUuid, (id) => `conformance:${label}-${id}`), SELECTIONS = new Set(["model", "codeModel", "fastModel", "fallbackModel"]), COUNTS = new Set(["maxAttempts", "budgetMillis"]), restorable = (field, value) => SELECTIONS.has(field) ? Option.isSome(parseModelSelection(value)) : COUNTS.has(field) ? Number.isInteger(Number(value)) && Number(value) >= 1 : !0, restoring = (store, id, field, write) => (body) => store.load.pipe(Effect.flatMap((before) => Option.match(Option.filter(valueOf(before, field), (value) => !restorable(field, value)), {
|
|
7
|
+
onSome: (value) => fail(id, `the store holds ${field} "${value}", which a conforming store refuses to write, so the kit could not put it back; it changed nothing. Correct the value, then run the kit`),
|
|
8
|
+
onNone: () => body.pipe(Effect.ensuring(store.load.pipe(Effect.flatMap((after) => Option.getOrNull(valueOf(after, field)) === Option.getOrNull(valueOf(before, field)) ? Effect.void : write(valueOf(before, field))), Effect.ignore({ log: "Warn", message: `the kit could not put back ${field}` }))))
|
|
9
|
+
}))), restoringRole = (store, id, role) => restoring(store, id, ROLE_FIELDS[role], (value) => store.setRole(role, value)), restoringKey = (store, id, key) => restoring(store, id, key, (value) => store.set(key, value)), refused = (store, id, what, write) => Effect.gen(function* () {
|
|
10
|
+
const before = snapshot(yield* store.load), result = yield* Effect.result(write);
|
|
11
|
+
if (result._tag === "Success")
|
|
12
|
+
return yield* fail(id, `${what} was accepted`);
|
|
13
|
+
const tag = typeof result.failure === "object" && result.failure !== null && "_tag" in result.failure ? String(result.failure._tag) : "an error";
|
|
14
|
+
yield* expect(id, tag === "SettingsError", `${what} failed with ${tag}, not a SettingsError`);
|
|
15
|
+
yield* expect(id, snapshot(yield* store.load) === before, `${what} changed the settings`);
|
|
16
|
+
}), CANONICAL = [
|
|
17
|
+
{ key: "maxAttempts", text: "3", reads: 3 },
|
|
18
|
+
{ key: "budgetMillis", text: "60000", reads: 60000 },
|
|
19
|
+
{ key: "reasoningEffort", text: "high", reads: "high" }
|
|
20
|
+
], CLEARED = [
|
|
21
|
+
{ key: "maxAttempts", texts: ["4", "5"] },
|
|
22
|
+
{ key: "budgetMillis", texts: ["1000", "2000"] },
|
|
23
|
+
{ key: "reasoningEffort", texts: ["low", "medium"] }
|
|
24
|
+
], BAD = [
|
|
25
|
+
{ key: "fallbackModel", text: "not-a-selection" },
|
|
26
|
+
{ key: "maxAttempts", text: "0" },
|
|
27
|
+
{ key: "maxAttempts", text: "two" },
|
|
28
|
+
{ key: "budgetMillis", text: "1.5" },
|
|
29
|
+
{ key: "reasoningEffort", text: "extreme" }
|
|
30
|
+
], corrupted = (what, effect) => Effect.result(effect).pipe(Effect.flatMap((result) => {
|
|
31
|
+
if (result._tag === "Success")
|
|
32
|
+
return fail("corrupt", `${what} succeeded on a store whose backing data cannot be read`);
|
|
33
|
+
const error = result.failure, tag = typeof error === "object" && error !== null && "_tag" in error ? String(error._tag) : "an error", code = Option.getOrElse(harnessCodeOf(error), () => "no code");
|
|
34
|
+
return expect("corrupt", tag === "SettingsError" && code === "settings.corrupt", `${what} failed with ${tag} (${code}), not a SettingsError of code settings.corrupt`);
|
|
35
|
+
})), corruptCheck = (store, writable, corrupt) => check("a store whose backing data cannot be read fails load and every write with a SettingsError of code settings.corrupt, never reads it as defaults", "corrupt", () => {
|
|
36
|
+
const body = Effect.gen(function* () {
|
|
37
|
+
const before = snapshot(yield* store.load);
|
|
38
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
39
|
+
yield* corrupt;
|
|
40
|
+
yield* corrupted("load", store.load);
|
|
41
|
+
yield* writable ? Effect.all([
|
|
42
|
+
selection("corrupt").pipe(Effect.flatMap((value) => corrupted("setRole", store.setRole("general", Option.some(value))))),
|
|
43
|
+
corrupted("set", store.set("maxAttempts", Option.some("3")))
|
|
44
|
+
], { concurrency: 1, discard: !0 }) : Effect.void;
|
|
45
|
+
}));
|
|
46
|
+
yield* expect("corrupt", snapshot(yield* store.load) === before, "once the backing data is repaired, the store does not read what it held before");
|
|
47
|
+
});
|
|
48
|
+
return writable ? body.pipe(restoringRole(store, "corrupt", "general"), restoringKey(store, "corrupt", "maxAttempts")) : body;
|
|
49
|
+
});
|
|
50
|
+
export const settingsStoreConformance = (store, options) => [
|
|
51
|
+
check("load is stable: two loads with no write between them agree", "load", () => Effect.gen(function* () {
|
|
52
|
+
const first = snapshot(yield* store.load);
|
|
53
|
+
yield* expect("load", snapshot(yield* store.load) === first, "two loads disagree");
|
|
54
|
+
})),
|
|
55
|
+
...options.writable ? writableChecks(store) : readOnlyChecks(store),
|
|
56
|
+
...Option.match(Option.fromNullishOr(options.corrupt), { onNone: () => [], onSome: (corrupt) => [corruptCheck(store, options.writable, corrupt)] })
|
|
57
|
+
];
|
|
58
|
+
const writableChecks = (store) => [
|
|
59
|
+
check("each role reads back what setRole wrote, and clearing it returns the value before the set", "role", () => Effect.forEach(ROLES, (role) => Effect.gen(function* () {
|
|
60
|
+
const field = ROLE_FIELDS[role];
|
|
61
|
+
yield* store.setRole(role, Option.none());
|
|
62
|
+
const base = yield* store.load, value = yield* selection(role);
|
|
63
|
+
yield* store.setRole(role, Option.some(value));
|
|
64
|
+
const set = yield* store.load;
|
|
65
|
+
yield* expect("role", Option.getOrUndefined(valueOf(set, field)) === value, `the ${role} role reads ${Option.getOrElse(valueOf(set, field), () => "nothing")}, not what was set`);
|
|
66
|
+
yield* expect("role", others(set, field) === others(base, field), `setting the ${role} role changed another value`);
|
|
67
|
+
yield* store.setRole(role, Option.none());
|
|
68
|
+
yield* expect("role", snapshot(yield* store.load) === snapshot(base), `clearing the ${role} role did not return the value before the set`);
|
|
69
|
+
}).pipe(restoringRole(store, "role", role)), { concurrency: 1, discard: !0 })),
|
|
70
|
+
check("set accepts each key's canonical string form and load reads it back typed", "values", () => selection("fallback").pipe(Effect.flatMap((fallback) => Effect.forEach([
|
|
71
|
+
...CANONICAL,
|
|
72
|
+
{ key: "fallbackModel", text: fallback, reads: void 0 }
|
|
73
|
+
], ({ key, text, reads }) => Effect.gen(function* () {
|
|
74
|
+
const before = yield* store.load;
|
|
75
|
+
yield* store.set(key, Option.some(text));
|
|
76
|
+
const after = yield* store.load, expected = reads ?? text, read = Option.getOrNull(fieldOf(after, key));
|
|
77
|
+
yield* expect("values", read === expected, `${key} "${text}" reads back as ${JSON.stringify(read)}`);
|
|
78
|
+
yield* expect("values", others(after, key) === others(before, key), `setting ${key} changed another value`);
|
|
79
|
+
}).pipe(restoringKey(store, "values", key)), { concurrency: 1, discard: !0 })))),
|
|
80
|
+
check("None clears a key back to the value before the set", "clear", () => Effect.forEach(CLEARED, ({ key, texts }) => Effect.gen(function* () {
|
|
81
|
+
yield* store.set(key, Option.none());
|
|
82
|
+
const base = yield* store.load, text = texts.find((candidate) => Option.getOrNull(valueOf(base, key)) !== candidate) ?? texts[0];
|
|
83
|
+
yield* store.set(key, Option.some(text));
|
|
84
|
+
yield* store.set(key, Option.none());
|
|
85
|
+
yield* expect("clear", snapshot(yield* store.load) === snapshot(base), `clearing ${key} did not return the value before the set`);
|
|
86
|
+
}).pipe(restoringKey(store, "clear", key)), { concurrency: 1, discard: !0 })),
|
|
87
|
+
check("set refuses a value that does not parse with a SettingsError and changes nothing", "bad-values", () => Effect.forEach(BAD, ({ key, text }) => refused(store, "bad-values", `${key} "${text}"`, store.set(key, Option.some(text))).pipe(restoringKey(store, "bad-values", key)), { concurrency: 1, discard: !0 })),
|
|
88
|
+
check('setRole refuses a selection that is not "<provider>:<modelId>" with a SettingsError and changes nothing', "bad-role", () => Effect.forEach(ROLES, (role) => Effect.forEach(["not-a-selection", ":model", "provider:"], (text) => refused(store, "bad-role", `the ${role} role "${text}"`, store.setRole(role, Option.some(text))), { concurrency: 1, discard: !0 }).pipe(restoringRole(store, "bad-role", role)), { concurrency: 1, discard: !0 }))
|
|
89
|
+
], readOnlyChecks = (store) => [
|
|
90
|
+
check("a read-only store refuses every write with a SettingsError and changes nothing", "read-only", () => Effect.gen(function* () {
|
|
91
|
+
yield* Effect.forEach(ROLES, (role) => Effect.all([
|
|
92
|
+
selection(role).pipe(Effect.flatMap((value) => refused(store, "read-only", `setRole ${role}`, store.setRole(role, Option.some(value))))),
|
|
93
|
+
refused(store, "read-only", `clearing the ${role} role`, store.setRole(role, Option.none()))
|
|
94
|
+
], { concurrency: 1 }), { concurrency: 1, discard: !0 });
|
|
95
|
+
yield* Effect.forEach(CANONICAL, ({ key, text }) => Effect.all([
|
|
96
|
+
refused(store, "read-only", `set ${key}`, store.set(key, Option.some(text))),
|
|
97
|
+
refused(store, "read-only", `clearing ${key}`, store.set(key, Option.none()))
|
|
98
|
+
], { concurrency: 1 }), { concurrency: 1, discard: !0 });
|
|
99
|
+
}))
|
|
100
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A settings value as a config file holds it: a selection or other text, a switch, a count.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export declare const SettingValue: Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>;
|
|
8
|
+
export type SettingValue = typeof SettingValue.Type;
|
|
9
|
+
/**
|
|
10
|
+
* The settings field each model role is kept under.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export declare const RoleField: Schema.Literals<readonly ["model", "codeModel", "fastModel"]>;
|
|
15
|
+
export type RoleField = typeof RoleField.Type;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { EngineSettings, SettingsError } from "../ports/settings-store.port.js";
|
|
3
|
+
import type { ModelRole, SettingsKey } from "../ports/settings-store.port.js";
|
|
4
|
+
import type { RoleField, SettingValue } from "./settings-value.entity.js";
|
|
5
|
+
/**
|
|
6
|
+
* The settings vocabulary every SettingsStore of this package shares: how a
|
|
7
|
+
* config record reads as `EngineSettings`, how a value's string form (what
|
|
8
|
+
* `set` receives: "true", "3", "provider:model") is checked and coerced, and
|
|
9
|
+
* how one key or role changes a settings value.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The field each model role is kept under.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export declare const ROLE_FIELDS: Readonly<Record<ModelRole, RoleField>>;
|
|
17
|
+
/**
|
|
18
|
+
* Settings from a config record: values of the wrong type read as unset, keys it does not name are not read; the legacy `openAiReasoningEffort` key backs `reasoningEffort`.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export declare const settingsFromRecord: (record: Readonly<Record<string, unknown>>) => EngineSettings;
|
|
23
|
+
/** Settings as a config record: the set fields only. */
|
|
24
|
+
export declare const recordOfSettings: (settings: EngineSettings) => Readonly<Record<string, SettingValue>>;
|
|
25
|
+
/**
|
|
26
|
+
* One key's value from its string form, checked: a selection must be
|
|
27
|
+
* `"<provider>:<modelId>"`, an effort one the models accept, a count a
|
|
28
|
+
* positive integer. A value that does not parse is
|
|
29
|
+
* refused, not written and then ignored on load.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export declare const coerceSettingValue: (key: SettingsKey, raw: string) => Effect.Effect<SettingValue, SettingsError>;
|
|
34
|
+
/**
|
|
35
|
+
* A role's selection, checked: `None` clears the role; a value must be `"<provider>:<modelId>"`.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare const roleSelection: (role: ModelRole, selection: Option.Option<string>) => Effect.Effect<Option.Option<string>, SettingsError>;
|
|
40
|
+
/**
|
|
41
|
+
* `settings` with one key set from its string form (checked, as `set` takes it), or cleared with `None`.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export declare const applySetting: (settings: EngineSettings, key: SettingsKey, value: Option.Option<string>) => Effect.Effect<EngineSettings, SettingsError>;
|
|
46
|
+
/**
|
|
47
|
+
* `settings` with one role's selection set (checked, as `setRole` takes it), or cleared with `None`.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare const applyRole: (settings: EngineSettings, role: ModelRole, selection: Option.Option<string>) => Effect.Effect<EngineSettings, SettingsError>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect, Match, Option, Schema } from "effect";
|
|
2
|
+
import { parseModelSelection, ReasoningEffort } from "@xandreed/core";
|
|
3
|
+
import { EngineSettings, SETTINGS_KEYS, SettingsError } from "../ports/settings-store.port.js";
|
|
4
|
+
export const ROLE_FIELDS = {
|
|
5
|
+
general: "model",
|
|
6
|
+
code: "codeModel",
|
|
7
|
+
fast: "fastModel"
|
|
8
|
+
};
|
|
9
|
+
const FIELDS = ["model", "codeModel", "fastModel", ...SETTINGS_KEYS], asString = Schema.decodeUnknownOption(Schema.NonEmptyString), asNumber = Schema.decodeUnknownOption(Schema.Finite), asReasoningEffort = Schema.decodeUnknownOption(ReasoningEffort);
|
|
10
|
+
export const settingsFromRecord = (record) => new EngineSettings({
|
|
11
|
+
model: asString(record.model),
|
|
12
|
+
codeModel: asString(record.codeModel),
|
|
13
|
+
fastModel: asString(record.fastModel),
|
|
14
|
+
fallbackModel: asString(record.fallbackModel),
|
|
15
|
+
maxAttempts: asNumber(record.maxAttempts),
|
|
16
|
+
budgetMillis: asNumber(record.budgetMillis),
|
|
17
|
+
reasoningEffort: Option.orElse(asReasoningEffort(record.reasoningEffort), () => asReasoningEffort(record.openAiReasoningEffort)),
|
|
18
|
+
codeReasoningEffort: asReasoningEffort(record.codeReasoningEffort),
|
|
19
|
+
fastReasoningEffort: asReasoningEffort(record.fastReasoningEffort)
|
|
20
|
+
}), recordOfSettings = (settings) => Object.fromEntries(FIELDS.flatMap((field) => Option.match(fieldOf(settings, field), {
|
|
21
|
+
onNone: () => [],
|
|
22
|
+
onSome: (value) => [[field, value]]
|
|
23
|
+
})));
|
|
24
|
+
const fieldOf = (settings, field) => settings[field], refuse = (message) => Effect.fail(new SettingsError({ message }));
|
|
25
|
+
export const coerceSettingValue = (key, raw) => Match.value(key).pipe(Match.when("fallbackModel", () => Option.match(parseModelSelection(raw), {
|
|
26
|
+
onNone: () => refuse(`fallbackModel must be "<provider>:<modelId>" \u2014 got "${raw}"`),
|
|
27
|
+
onSome: () => Effect.succeed(raw)
|
|
28
|
+
})), Match.whenOr("reasoningEffort", "codeReasoningEffort", "fastReasoningEffort", (effort) => Option.match(asReasoningEffort(raw), {
|
|
29
|
+
onNone: () => refuse(`${effort} is not supported: "${raw}"`),
|
|
30
|
+
onSome: (value) => Effect.succeed(value)
|
|
31
|
+
})), Match.whenOr("maxAttempts", "budgetMillis", (count) => Number.isInteger(Number(raw)) && Number(raw) >= 1 ? Effect.succeed(Number(raw)) : refuse(`${count} must be a positive integer \u2014 got "${raw}"`)), Match.exhaustive), roleSelection = (role, selection) => Option.match(selection, {
|
|
32
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
33
|
+
onSome: (raw) => Option.isSome(parseModelSelection(raw)) ? Effect.succeed(Option.some(raw)) : refuse(`the ${role} model must be "<provider>:<modelId>" \u2014 got "${raw}"`)
|
|
34
|
+
});
|
|
35
|
+
const withField = (settings, field, value) => {
|
|
36
|
+
const others = Object.fromEntries(Object.entries(recordOfSettings(settings)).filter(([key]) => key !== field));
|
|
37
|
+
return settingsFromRecord(Option.match(value, { onNone: () => others, onSome: (set) => ({ ...others, [field]: set }) }));
|
|
38
|
+
};
|
|
39
|
+
export const applySetting = (settings, key, value) => Option.match(value, {
|
|
40
|
+
onNone: () => Effect.succeed(withField(settings, key, Option.none())),
|
|
41
|
+
onSome: (raw) => coerceSettingValue(key, raw).pipe(Effect.map((coerced) => withField(settings, key, Option.some(coerced))))
|
|
42
|
+
}), applyRole = (settings, role, selection) => roleSelection(role, selection).pipe(Effect.map((checked) => withField(settings, ROLE_FIELDS[role], checked)));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Semaphore } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Read the JSON object in `path`: `None` when no file is there. A file that
|
|
4
|
+
* cannot be read (any error but ENOENT: permissions, a directory) or does
|
|
5
|
+
* not hold a JSON object fails with `unusable(reason)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const readJsonObject: <E>(path: string, unusable: (reason: string) => E) => Effect.Effect<Option.Option<Schema.JsonObject>, E>;
|
|
8
|
+
/**
|
|
9
|
+
* Replace `path` with `value`: written to a temp file of its own beside it
|
|
10
|
+
* (named by the process and a random number, so concurrent writes never
|
|
11
|
+
* share one), given `mode` when there is one, then renamed over. A failed
|
|
12
|
+
* write removes its temp file.
|
|
13
|
+
*/
|
|
14
|
+
export declare const writeJsonObject: <E>(path: string, value: Record<string, unknown>, mode: Option.Option<number>, failed: (reason: string) => E) => Effect.Effect<void, E>;
|
|
15
|
+
/** A lock of its own for `name` at `path` (one refresh of a provider's token kept there, say). */
|
|
16
|
+
export declare const namedLock: (path: string, name: string) => Effect.Effect<Semaphore.Semaphore>;
|
|
17
|
+
/** Run `effect` holding the lock of `path`, which every store in the process that writes the file shares. */
|
|
18
|
+
export declare const withFileLock: (path: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { Effect, Option, Random, Schema, Semaphore } from "effect";
|
|
4
|
+
import { errorCodeOf } from "@xandreed/core";
|
|
5
|
+
const decodeJsonObject = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.JsonObject)), isMissing = (cause) => Option.contains(errorCodeOf(cause), "ENOENT");
|
|
6
|
+
export const readJsonObject = (path, unusable) => Effect.tryPromise({ try: () => readFile(path, "utf-8"), catch: (cause) => cause }).pipe(Effect.map(Option.some), Effect.catchIf(isMissing, () => Effect.succeed(Option.none())), Effect.mapError((cause) => unusable(`could not be read (${Option.getOrElse(errorCodeOf(cause), () => String(cause))})`)), Effect.flatMap(Option.match({
|
|
7
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
8
|
+
onSome: (text) => decodeJsonObject(text).pipe(Effect.map(Option.some), Effect.mapError(() => unusable("does not hold a JSON object")))
|
|
9
|
+
}))), writeJsonObject = (path, value, mode, failed) => Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER).pipe(Effect.flatMap((suffix) => {
|
|
10
|
+
const tmp = `${path}.tmp-${process.pid}-${suffix.toString(36)}`;
|
|
11
|
+
return Effect.tryPromise({
|
|
12
|
+
try: async () => {
|
|
13
|
+
await mkdir(dirname(path), { recursive: !0 });
|
|
14
|
+
await writeFile(tmp, JSON.stringify(value, null, 2), "utf-8");
|
|
15
|
+
await Option.match(mode, { onNone: () => Promise.resolve(), onSome: (bits) => chmod(tmp, bits) });
|
|
16
|
+
await rename(tmp, path);
|
|
17
|
+
},
|
|
18
|
+
catch: (cause) => failed(String(cause))
|
|
19
|
+
}).pipe(Effect.tapError(() => Effect.tryPromise({ try: () => rm(tmp, { force: !0 }), catch: (cause) => cause }).pipe(Effect.catch((cause) => Effect.logWarning(`could not remove the temporary file ${tmp}`, cause)))));
|
|
20
|
+
}));
|
|
21
|
+
const locks = new Map, lockOf = (key) => Effect.sync(() => Option.getOrElse(Option.fromNullishOr(locks.get(key)), () => {
|
|
22
|
+
const made = Semaphore.makeUnsafe(1);
|
|
23
|
+
locks.set(key, made);
|
|
24
|
+
return made;
|
|
25
|
+
}));
|
|
26
|
+
export const namedLock = (path, name) => lockOf(`${name}:${resolve(path)}`), withFileLock = (path) => (effect) => Effect.flatMap(namedLock(path, "file"), (lock) => lock.withPermit(effect));
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A proxy URL as an option takes it: a string, or a `Redacted` one when it carries credentials.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export declare const ProxyUrlOption: Schema.Union<readonly [Schema.String, Schema.Redacted<Schema.String>]>;
|
|
8
|
+
export type ProxyUrlOption = typeof ProxyUrlOption.Type;
|
|
9
|
+
/**
|
|
10
|
+
* The proxy a host gives the models plugin, as its `proxy` and `noProxy`
|
|
11
|
+
* options. `proxy` is one URL for every model and OAuth call (`http://` or
|
|
12
|
+
* `https://`; a value without a scheme reads as `http://`); it may carry
|
|
13
|
+
* credentials (`http://user:pass@proxy:8080`), so a host's code can pass it
|
|
14
|
+
* `Redacted`. Empty or absent, the environment's `https_proxy` /
|
|
15
|
+
* `HTTPS_PROXY` and `http_proxy` / `HTTP_PROXY` apply. `noProxy` lists the
|
|
16
|
+
* hosts that go direct, in NO_PROXY syntax, added to the environment's
|
|
17
|
+
* `no_proxy` / `NO_PROXY`; `"*"` turns the proxy off.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export declare const HttpProxyOptions: Schema.Struct<{
|
|
22
|
+
readonly proxy: Schema.optionalKey<Schema.Union<readonly [Schema.String, Schema.Redacted<Schema.String>]>>;
|
|
23
|
+
readonly noProxy: Schema.optionalKey<Schema.String>;
|
|
24
|
+
}>;
|
|
25
|
+
export type HttpProxyOptions = typeof HttpProxyOptions.Type;
|
|
26
|
+
/**
|
|
27
|
+
* The proxy variables of the environment, as read (an empty value counts as
|
|
28
|
+
* unset). Lower case is read before upper case, as undici (Node's
|
|
29
|
+
* environment proxy) and curl read them. `ALL_PROXY` is not read: SOCKS
|
|
30
|
+
* proxies are not supported.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ProxyEnvironment: Schema.Struct<{
|
|
33
|
+
readonly https_proxy: Schema.optionalKey<Schema.String>;
|
|
34
|
+
readonly HTTPS_PROXY: Schema.optionalKey<Schema.String>;
|
|
35
|
+
readonly http_proxy: Schema.optionalKey<Schema.String>;
|
|
36
|
+
readonly HTTP_PROXY: Schema.optionalKey<Schema.String>;
|
|
37
|
+
readonly no_proxy: Schema.optionalKey<Schema.String>;
|
|
38
|
+
readonly NO_PROXY: Schema.optionalKey<Schema.String>;
|
|
39
|
+
}>;
|
|
40
|
+
export type ProxyEnvironment = typeof ProxyEnvironment.Type;
|
|
41
|
+
/** The environment variables a `ProxyEnvironment` is read from. */
|
|
42
|
+
export declare const PROXY_VARIABLES: readonly ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY", "no_proxy", "NO_PROXY"];
|
|
43
|
+
/**
|
|
44
|
+
* One NO_PROXY entry: every host (`*`); a host name and its subdomains
|
|
45
|
+
* (`example.com`, `.example.com` and `*.example.com` alike); or an IP
|
|
46
|
+
* address, exact (`10.1.2.3`, `::1`, `[::1]`) or a range (`10.0.0.0/8`,
|
|
47
|
+
* `fd00::/8`, as `prefix` leading bits). A name or an exact address may
|
|
48
|
+
* carry a port (`example.com:8443`, `[::1]:8080`), which then must match.
|
|
49
|
+
*/
|
|
50
|
+
export declare const NoProxyRule: Schema.Union<readonly [Schema.TaggedStruct<"Everything", {}>, Schema.TaggedStruct<"Domain", {
|
|
51
|
+
readonly name: Schema.String;
|
|
52
|
+
readonly port: Schema.Option<Schema.Int>;
|
|
53
|
+
}>, Schema.TaggedStruct<"Address", {
|
|
54
|
+
readonly bytes: Schema.$Array<Schema.Int>;
|
|
55
|
+
readonly prefix: Schema.Int;
|
|
56
|
+
readonly port: Schema.Option<Schema.Int>;
|
|
57
|
+
}>]>;
|
|
58
|
+
export type NoProxyRule = typeof NoProxyRule.Type;
|
|
59
|
+
declare const ProxyConfig_base: Schema.Class<ProxyConfig, Schema.Struct<{
|
|
60
|
+
readonly https: Schema.Option<Schema.Redacted<Schema.String>>;
|
|
61
|
+
readonly http: Schema.Option<Schema.Redacted<Schema.String>>;
|
|
62
|
+
readonly noProxy: Schema.$Array<Schema.Union<readonly [Schema.TaggedStruct<"Everything", {}>, Schema.TaggedStruct<"Domain", {
|
|
63
|
+
readonly name: Schema.String;
|
|
64
|
+
readonly port: Schema.Option<Schema.Int>;
|
|
65
|
+
}>, Schema.TaggedStruct<"Address", {
|
|
66
|
+
readonly bytes: Schema.$Array<Schema.Int>;
|
|
67
|
+
readonly prefix: Schema.Int;
|
|
68
|
+
readonly port: Schema.Option<Schema.Int>;
|
|
69
|
+
}>]>>;
|
|
70
|
+
}>, {}>;
|
|
71
|
+
/**
|
|
72
|
+
* A resolved proxy setting: the proxy for `https:` (and `wss:`) targets,
|
|
73
|
+
* the one for `http:` (and `ws:`) targets, and the hosts that go direct.
|
|
74
|
+
* Proxy URLs stay `Redacted`, so a printed setting never shows credentials.
|
|
75
|
+
* Loopback hosts always go direct.
|
|
76
|
+
*/
|
|
77
|
+
export declare class ProxyConfig extends ProxyConfig_base {
|
|
78
|
+
}
|
|
79
|
+
declare const ProxyInvalid_base: Schema.Class<ProxyInvalid, Schema.TaggedStruct<"ProxyInvalid", {
|
|
80
|
+
readonly source: Schema.String;
|
|
81
|
+
readonly message: Schema.String;
|
|
82
|
+
}>, import("effect/Cause").YieldableError>;
|
|
83
|
+
/**
|
|
84
|
+
* A proxy URL that cannot be used: not a URL, or not `http://` / `https://`. `source` names where it came from.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export declare class ProxyInvalid extends ProxyInvalid_base {
|
|
89
|
+
}
|
|
90
|
+
declare const ProxyUnsupported_base: Schema.Class<ProxyUnsupported, Schema.TaggedStruct<"ProxyUnsupported", {
|
|
91
|
+
readonly message: Schema.String;
|
|
92
|
+
}>, import("effect/Cause").YieldableError>;
|
|
93
|
+
/**
|
|
94
|
+
* A proxy applies, but this runtime cannot send the call through it: the call is refused rather than sent direct.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
export declare class ProxyUnsupported extends ProxyUnsupported_base {
|
|
99
|
+
}
|
|
100
|
+
/** The first Bun whose native `WebSocket` sends through its `proxy` option (1.3.0 to 1.3.5 connect directly). */
|
|
101
|
+
export declare const BUN_WEBSOCKET_PROXY_VERSION = "1.3.6";
|
|
102
|
+
/** The first Node whose `https.Agent` tunnels through its `proxyEnv` option (earlier ones connect directly). */
|
|
103
|
+
export declare const NODE_WEBSOCKET_PROXY_VERSION = "24.5.0";
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Option, Redacted, Result } from "effect";
|
|
2
|
+
import { ProxyConfig, ProxyInvalid } from "./http-proxy.entity.js";
|
|
3
|
+
import type { HttpProxyOptions, NoProxyRule, ProxyEnvironment } from "./http-proxy.entity.js";
|
|
4
|
+
/**
|
|
5
|
+
* Credentials in the URLs of `text` (`scheme://user:pass@host`) replaced by
|
|
6
|
+
* `<redacted>`, for error messages that may quote a proxy URL.
|
|
7
|
+
*/
|
|
8
|
+
export declare const redactCredentials: (text: string) => string;
|
|
9
|
+
/** A proxy URL as a message may show it: its scheme and host, its credentials (if any) as `<redacted>`. */
|
|
10
|
+
export declare const displayProxy: (url: Redacted.Redacted<string>) => string;
|
|
11
|
+
/**
|
|
12
|
+
* One proxy URL, checked: `None` when blank; a value without a scheme
|
|
13
|
+
* (`proxy.corp:8080`) reads as `http://`; anything that is not an `http://`
|
|
14
|
+
* or `https://` URL with a host fails with `ProxyInvalid` naming `source`,
|
|
15
|
+
* credentials redacted.
|
|
16
|
+
*/
|
|
17
|
+
export declare const proxyUrl: (raw: string, source: string) => Result.Result<Option.Option<Redacted.Redacted<string>>, ProxyInvalid>;
|
|
18
|
+
/** The IPv4 address `text` spells, as its 4 bytes. */
|
|
19
|
+
export declare const parseIpv4: (text: string) => Option.Option<ReadonlyArray<number>>;
|
|
20
|
+
/** The IPv6 address `text` spells (brackets and a zone id allowed), as its 16 bytes. */
|
|
21
|
+
export declare const parseIpv6: (text: string) => Option.Option<ReadonlyArray<number>>;
|
|
22
|
+
/** A host is loopback: `localhost` (and its subdomains), `127.0.0.0/8`, `::1`, or IPv4 loopback mapped into IPv6. */
|
|
23
|
+
export declare const isLoopback: (hostname: string) => boolean;
|
|
24
|
+
/** A NO_PROXY value (entries separated by commas or white space), parsed. */
|
|
25
|
+
export declare const parseNoProxy: (value: string) => ReadonlyArray<NoProxyRule>;
|
|
26
|
+
/** Whether a NO_PROXY rule sends `target` direct. */
|
|
27
|
+
export declare const bypassesProxy: (rules: ReadonlyArray<NoProxyRule>, target: URL) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The proxy settings of the models plugin's options over the environment's:
|
|
30
|
+
* an explicit `proxy` serves every target; otherwise `https_proxy` (then
|
|
31
|
+
* `HTTPS_PROXY`) serves `https:` targets, `http_proxy` (then `HTTP_PROXY`)
|
|
32
|
+
* `http:` ones, and an `https:` target falls back to the `http:` proxy, as
|
|
33
|
+
* undici's environment proxy (Node's, under `NODE_USE_ENV_PROXY`) does. The
|
|
34
|
+
* direct hosts are `no_proxy` (then `NO_PROXY`) and the `noProxy` option
|
|
35
|
+
* together; with `*` among them the environment's proxy variables are not
|
|
36
|
+
* read at all. Lower case comes first, as undici and curl read them. A proxy
|
|
37
|
+
* URL that cannot be used fails with `ProxyInvalid`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const resolveProxy: (options: HttpProxyOptions, env: ProxyEnvironment) => Result.Result<ProxyConfig, ProxyInvalid>;
|
|
40
|
+
/** Whether any target can go through a proxy. */
|
|
41
|
+
export declare const usesProxy: (config: ProxyConfig) => boolean;
|
|
42
|
+
/** Whether the setting turns the proxy off for every host (`*` in `noProxy` or `NO_PROXY`). */
|
|
43
|
+
export declare const turnsProxyOff: (config: ProxyConfig) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The proxy a request to `target` goes through, or `None` for direct:
|
|
46
|
+
* loopback hosts and the `noProxy` hosts go direct; `https:` and `wss:`
|
|
47
|
+
* targets use the https proxy, `http:` and `ws:` ones the http proxy.
|
|
48
|
+
*/
|
|
49
|
+
export declare const proxyFor: (target: URL, config: ProxyConfig) => Option.Option<Redacted.Redacted<string>>;
|
|
50
|
+
/** Whether a runtime `version` (`24.16.0`, `1.3.6`) is at least `minimum`; an unreadable one is not. */
|
|
51
|
+
export declare const atLeastVersion: (version: string, minimum: string) => boolean;
|