@xandreed/plugin-models 0.12.0 → 0.14.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 +16 -2
- package/dist/auth/anthropicOAuth.js +14 -11
- package/dist/auth/credential-input.entity.d.ts +34 -0
- package/dist/auth/credential-input.entity.functions.d.ts +8 -0
- package/dist/auth/credential-input.entity.functions.js +9 -0
- package/dist/auth/credential-input.entity.js +20 -0
- package/dist/auth/credential-refresh.entity.d.ts +9 -0
- package/dist/auth/credential-refresh.entity.functions.d.ts +15 -0
- package/dist/auth/credential-refresh.entity.functions.js +12 -0
- package/dist/auth/credential-refresh.entity.functions.test.d.ts +1 -0
- package/dist/auth/credential-refresh.entity.js +2 -0
- package/dist/auth/localAuth.d.ts +2 -1
- package/dist/auth/localAuth.js +41 -43
- package/dist/auth/memory-auth.adapter.d.ts +27 -0
- package/dist/auth/memory-auth.adapter.js +47 -0
- package/dist/auth/memory-auth.adapter.test.d.ts +1 -0
- package/dist/auth/openAiCodexOAuth.d.ts +3 -2
- package/dist/auth/openAiCodexOAuth.js +9 -8
- package/dist/index.d.ts +23 -6
- package/dist/index.js +28 -1
- package/dist/llm/compat.d.ts +39 -8
- package/dist/llm/compat.js +80 -29
- package/dist/llm/modelCatalog.d.ts +20 -1
- package/dist/llm/modelCatalog.js +19 -7
- package/dist/llm/openAiCodex.d.ts +4 -1
- package/dist/llm/openAiCodex.js +2 -2
- package/dist/llm/openAiCodexWebSocket.d.ts +23 -3
- package/dist/llm/openAiCodexWebSocket.js +125 -71
- package/dist/llm/openAiCodexWebSocket.test.d.ts +1 -0
- package/dist/llm/providers.d.ts +18 -4
- package/dist/llm/providers.js +49 -24
- package/dist/llm/router.d.ts +11 -1
- package/dist/llm/router.js +55 -31
- package/dist/llm/sse.d.ts +25 -3
- package/dist/llm/sse.js +8 -8
- package/dist/llm/utilityLlm.d.ts +1 -1
- package/dist/llm/utilityLlm.js +4 -4
- package/dist/plugin.adapter.d.ts +64 -4
- package/dist/plugin.adapter.js +48 -10
- package/dist/ports/host-credentials.port.d.ts +21 -0
- package/dist/ports/host-credentials.port.js +4 -0
- package/dist/ports/model-endpoints.port.d.ts +16 -3
- package/dist/ports/model-endpoints.port.js +7 -3
- package/dist/ports/models-transport.port.d.ts +46 -0
- package/dist/ports/models-transport.port.js +4 -0
- package/dist/settings/localSettings.js +6 -46
- package/dist/settings/memory-settings.adapter.d.ts +10 -0
- package/dist/settings/memory-settings.adapter.js +11 -0
- package/dist/settings/memory-settings.adapter.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.d.ts +7 -0
- package/dist/settings/settings-value.entity.functions.d.ts +29 -0
- package/dist/settings/settings-value.entity.functions.js +43 -0
- package/dist/settings/settings-value.entity.functions.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.js +2 -0
- package/dist/transport/http-proxy.entity.d.ts +90 -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.functions.test.d.ts +1 -0
- package/dist/transport/http-proxy.entity.js +42 -0
- package/dist/transport/models-transport.adapter.d.ts +48 -0
- package/dist/transport/models-transport.adapter.js +102 -0
- package/dist/transport/models-transport.adapter.test.d.ts +1 -0
- package/dist/transport/recording-proxy.testing.d.ts +33 -0
- package/dist/transport/undici-fetch.adapter.d.ts +13 -0
- package/dist/transport/undici-fetch.adapter.js +1 -0
- package/package.json +10 -9
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Match, Option, Redacted, Result, SemVer } from "effect";
|
|
2
|
+
import { ProxyConfig, ProxyInvalid } from "./http-proxy.entity.js";
|
|
3
|
+
const present = (value) => Option.filter(Option.fromNullishOr(value), (text) => text.trim().length > 0), firstSet = (values) => Option.fromNullishOr(values.flatMap(([name, value]) => Option.toArray(Option.map(present(value), (text) => [name, text.trim()])))[0]);
|
|
4
|
+
export const redactCredentials = (text) => text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1<redacted>@"), displayProxy = (url) => {
|
|
5
|
+
const raw = Redacted.value(url);
|
|
6
|
+
if (!URL.canParse(raw))
|
|
7
|
+
return redactCredentials(raw);
|
|
8
|
+
const parsed = new URL(raw), userinfo = parsed.username.length > 0 || parsed.password.length > 0 ? "<redacted>@" : "";
|
|
9
|
+
return `${parsed.protocol}//${userinfo}${parsed.host}`;
|
|
10
|
+
}, proxyUrl = (raw, source) => {
|
|
11
|
+
const text = raw.trim();
|
|
12
|
+
if (text.length === 0)
|
|
13
|
+
return Result.succeed(Option.none());
|
|
14
|
+
const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(text) ? text : `http://${text}`;
|
|
15
|
+
if (!URL.canParse(candidate))
|
|
16
|
+
return Result.fail(new ProxyInvalid({ source, message: `${source} is not a proxy URL: ${redactCredentials(candidate)}` }));
|
|
17
|
+
const url = new URL(candidate);
|
|
18
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
19
|
+
return Result.fail(new ProxyInvalid({
|
|
20
|
+
source,
|
|
21
|
+
message: `${source} is a ${url.protocol}// proxy; only http:// and https:// proxies are supported (SOCKS is not)`
|
|
22
|
+
}));
|
|
23
|
+
if (url.hostname.length === 0)
|
|
24
|
+
return Result.fail(new ProxyInvalid({ source, message: `${source} names no proxy host: ${redactCredentials(candidate)}` }));
|
|
25
|
+
return Result.succeed(Option.some(Redacted.make(url.href)));
|
|
26
|
+
}, parseIpv4 = (text) => {
|
|
27
|
+
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(text))
|
|
28
|
+
return Option.none();
|
|
29
|
+
const bytes = text.split(".").map(Number);
|
|
30
|
+
return bytes.every((byte) => byte <= 255) ? Option.some(bytes) : Option.none();
|
|
31
|
+
};
|
|
32
|
+
const ipv6Groups = (text) => {
|
|
33
|
+
const tail = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(text);
|
|
34
|
+
if (tail === null)
|
|
35
|
+
return Option.some(text);
|
|
36
|
+
const dotted = tail[1] ?? "";
|
|
37
|
+
return Option.map(parseIpv4(dotted), (bytes) => {
|
|
38
|
+
const [a = 0, b = 0, c = 0, d = 0] = bytes;
|
|
39
|
+
return `${text.slice(0, text.length - dotted.length)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export const parseIpv6 = (text) => Option.flatMap(ipv6Groups((text.replace(/^\[|\]$/g, "").split("%")[0] ?? "").toLowerCase()), (address) => {
|
|
43
|
+
const halves = address.split("::");
|
|
44
|
+
if (halves.length > 2 || !address.includes(":"))
|
|
45
|
+
return Option.none();
|
|
46
|
+
const groupsOf = (half) => half === void 0 || half.length === 0 ? [] : half.split(":"), head = groupsOf(halves[0]), tail = groupsOf(halves[1]), missing = 8 - head.length - tail.length;
|
|
47
|
+
if (halves.length === 1 ? missing !== 0 : missing < 1)
|
|
48
|
+
return Option.none();
|
|
49
|
+
const groups = [...head, ...Array.from({ length: halves.length === 1 ? 0 : missing }, () => "0"), ...tail];
|
|
50
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/.test(group)) ? Option.some(groups.flatMap((group) => {
|
|
51
|
+
const value = Number.parseInt(group, 16);
|
|
52
|
+
return [value >> 8, value & 255];
|
|
53
|
+
})) : Option.none();
|
|
54
|
+
});
|
|
55
|
+
const addressOf = (host) => Option.orElse(parseIpv4(host), () => parseIpv6(host)), inRange = (network, prefix, address) => network.length === address.length && network.every((byte, index) => {
|
|
56
|
+
const bits = Math.min(8, Math.max(0, prefix - index * 8)), mask = bits === 0 ? 0 : 255 << 8 - bits & 255;
|
|
57
|
+
return (byte & mask) === ((address[index] ?? 0) & mask);
|
|
58
|
+
});
|
|
59
|
+
export const isLoopback = (hostname) => {
|
|
60
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
61
|
+
if (host === "localhost" || host.endsWith(".localhost"))
|
|
62
|
+
return !0;
|
|
63
|
+
return Option.match(addressOf(host), {
|
|
64
|
+
onNone: () => !1,
|
|
65
|
+
onSome: (bytes) => bytes.length === 4 ? bytes[0] === 127 : inRange([...Array.from({ length: 15 }, () => 0), 1], 128, bytes) || inRange([...Array.from({ length: 10 }, () => 0), 255, 255, 127, 0, 0, 0], 104, bytes)
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const parseRule = (entry) => {
|
|
69
|
+
const text = entry.toLowerCase();
|
|
70
|
+
if (text === "*")
|
|
71
|
+
return Option.some({ _tag: "Everything" });
|
|
72
|
+
if (text.includes("/")) {
|
|
73
|
+
const [address = "", bits = ""] = text.split("/", 2);
|
|
74
|
+
return Option.flatMap(addressOf(address.replace(/^\[|\]$/g, "")), (bytes) => {
|
|
75
|
+
const prefix = Number(bits);
|
|
76
|
+
return /^\d+$/.test(bits) && prefix <= bytes.length * 8 ? Option.some({ _tag: "Address", bytes, prefix, port: Option.none() }) : Option.none();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const bracketed = /^\[([^\]]+)\](?::(\d+))?$/.exec(text), withPort = bracketed === null && (text.match(/:/g) ?? []).length === 1 ? /^(.+):(\d+)$/.exec(text) : null, host = bracketed?.[1] ?? withPort?.[1] ?? text, port = Option.map(Option.fromNullishOr(bracketed?.[2] ?? withPort?.[2]), Number);
|
|
80
|
+
return Option.some(Option.match(addressOf(host), {
|
|
81
|
+
onSome: (bytes) => ({ _tag: "Address", bytes, prefix: bytes.length * 8, port }),
|
|
82
|
+
onNone: () => ({ _tag: "Domain", name: host.replace(/^\*?\./, "").replace(/\.$/, ""), port })
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
export const parseNoProxy = (value) => value.split(/[\s,]+/).filter((entry) => entry.length > 0).flatMap((entry) => Option.toArray(parseRule(entry)));
|
|
86
|
+
const DEFAULT_PORTS = { "http:": 80, "ws:": 80, "https:": 443, "wss:": 443 };
|
|
87
|
+
export const bypassesProxy = (rules, target) => {
|
|
88
|
+
const host = target.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, ""), port = target.port.length > 0 ? Number(target.port) : DEFAULT_PORTS[target.protocol] ?? 0, address = addressOf(host), portMatches = (wanted) => Option.match(wanted, { onNone: () => !0, onSome: (value) => value === port });
|
|
89
|
+
return rules.some(Match.type().pipe(Match.tag("Everything", () => !0), Match.tag("Domain", (rule) => portMatches(rule.port) && (host === rule.name || host.endsWith(`.${rule.name}`))), Match.tag("Address", (rule) => portMatches(rule.port) && Option.exists(address, (bytes) => inRange(rule.bytes, rule.prefix, bytes))), Match.exhaustive));
|
|
90
|
+
};
|
|
91
|
+
const exemptsEverything = (rules) => rules.some((rule) => rule._tag === "Everything");
|
|
92
|
+
export const resolveProxy = (options, env) => Result.gen(function* () {
|
|
93
|
+
const explicit = yield* proxyUrl(options.proxy === void 0 ? "" : Redacted.isRedacted(options.proxy) ? Redacted.value(options.proxy) : options.proxy, "the proxy option"), noProxy = parseNoProxy([
|
|
94
|
+
...Option.toArray(Option.map(firstSet([["no_proxy", env.no_proxy], ["NO_PROXY", env.NO_PROXY]]), ([, value]) => value)),
|
|
95
|
+
options.noProxy ?? ""
|
|
96
|
+
].join(",")), off = exemptsEverything(noProxy), fromEnv = (names) => Option.match(off ? Option.none() : firstSet(names), {
|
|
97
|
+
onNone: () => Result.succeed(Option.none()),
|
|
98
|
+
onSome: ([name, value]) => proxyUrl(value, name)
|
|
99
|
+
}), http = Option.isSome(explicit) ? explicit : yield* fromEnv([["http_proxy", env.http_proxy], ["HTTP_PROXY", env.HTTP_PROXY]]), https = Option.isSome(explicit) ? explicit : Option.orElse(yield* fromEnv([["https_proxy", env.https_proxy], ["HTTPS_PROXY", env.HTTPS_PROXY]]), () => http);
|
|
100
|
+
return new ProxyConfig({ https, http, noProxy });
|
|
101
|
+
}), usesProxy = (config) => Option.isSome(config.https) || Option.isSome(config.http), turnsProxyOff = (config) => exemptsEverything(config.noProxy), proxyFor = (target, config) => {
|
|
102
|
+
if (isLoopback(target.hostname) || bypassesProxy(config.noProxy, target))
|
|
103
|
+
return Option.none();
|
|
104
|
+
if (target.protocol === "https:" || target.protocol === "wss:")
|
|
105
|
+
return config.https;
|
|
106
|
+
if (target.protocol === "http:" || target.protocol === "ws:")
|
|
107
|
+
return config.http;
|
|
108
|
+
return Option.none();
|
|
109
|
+
}, atLeastVersion = (version, minimum) => Result.match(Result.all([SemVer.parse(version.replace(/^v/, "")), SemVer.parse(minimum)]), {
|
|
110
|
+
onFailure: () => !1,
|
|
111
|
+
onSuccess: ([current, wanted]) => SemVer.isGreaterThanOrEqualTo(current, wanted)
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const ProxyUrlOption = Schema.Union([
|
|
3
|
+
Schema.String,
|
|
4
|
+
Schema.Redacted(Schema.String).annotate({ message: "proxy is a URL string or a Redacted one" })
|
|
5
|
+
]).annotate({ message: "proxy is a URL string or a Redacted one" }), HttpProxyOptions = Schema.Struct({
|
|
6
|
+
proxy: Schema.optionalKey(ProxyUrlOption),
|
|
7
|
+
noProxy: Schema.optionalKey(Schema.String)
|
|
8
|
+
}), ProxyEnvironment = Schema.Struct({
|
|
9
|
+
https_proxy: Schema.optionalKey(Schema.String),
|
|
10
|
+
HTTPS_PROXY: Schema.optionalKey(Schema.String),
|
|
11
|
+
http_proxy: Schema.optionalKey(Schema.String),
|
|
12
|
+
HTTP_PROXY: Schema.optionalKey(Schema.String),
|
|
13
|
+
no_proxy: Schema.optionalKey(Schema.String),
|
|
14
|
+
NO_PROXY: Schema.optionalKey(Schema.String)
|
|
15
|
+
}), PROXY_VARIABLES = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY", "no_proxy", "NO_PROXY"], NoProxyRule = Schema.Union([
|
|
16
|
+
Schema.TaggedStruct("Everything", {}),
|
|
17
|
+
Schema.TaggedStruct("Domain", { name: Schema.String, port: Schema.Option(Schema.Int) }),
|
|
18
|
+
Schema.TaggedStruct("Address", {
|
|
19
|
+
bytes: Schema.Array(Schema.Int),
|
|
20
|
+
prefix: Schema.Int,
|
|
21
|
+
port: Schema.Option(Schema.Int)
|
|
22
|
+
})
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export class ProxyConfig extends Schema.Class("ProxyConfig")({
|
|
26
|
+
https: Schema.Option(Schema.Redacted(Schema.String)),
|
|
27
|
+
http: Schema.Option(Schema.Redacted(Schema.String)),
|
|
28
|
+
noProxy: Schema.Array(NoProxyRule)
|
|
29
|
+
}) {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ProxyInvalid extends Schema.TaggedError()("ProxyInvalid", {
|
|
33
|
+
source: Schema.String,
|
|
34
|
+
message: Schema.String
|
|
35
|
+
}) {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ProxyUnsupported extends Schema.TaggedError()("ProxyUnsupported", {
|
|
39
|
+
message: Schema.String
|
|
40
|
+
}) {
|
|
41
|
+
}
|
|
42
|
+
export const BUN_WEBSOCKET_PROXY_VERSION = "1.3.6", NODE_WEBSOCKET_PROXY_VERSION = "24.5.0";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Config, Effect, Layer, Option } from "effect";
|
|
2
|
+
import type { Context, Scope } from "effect";
|
|
3
|
+
import { ModelsTransport } from "../ports/models-transport.port.js";
|
|
4
|
+
import { ProxyUnsupported } from "./http-proxy.entity.js";
|
|
5
|
+
import type { HttpProxyOptions, ProxyInvalid } from "./http-proxy.entity.js";
|
|
6
|
+
/** The runtime the plugin runs on, told apart without a Bun-only import (the subscription's WebSocket checks its route against it too). */
|
|
7
|
+
export type Runtime = {
|
|
8
|
+
readonly _tag: "Bun";
|
|
9
|
+
readonly version: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly _tag: "Node";
|
|
12
|
+
readonly version: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly _tag: "Other";
|
|
15
|
+
};
|
|
16
|
+
export declare const currentRuntime: () => Runtime;
|
|
17
|
+
/**
|
|
18
|
+
* The transport a proxy setting calls for, or `None` when no proxy applies
|
|
19
|
+
* and nothing has to be held direct: then the plugin adds no transport and
|
|
20
|
+
* every call uses the runtime's own fetch, as it did before the plugin knew
|
|
21
|
+
* proxies (a `FetchHttpClient.Fetch` the host provides still reaches the
|
|
22
|
+
* provider SDKs). With the proxy turned off (`*`) on Node it is a transport
|
|
23
|
+
* over undici's direct agent, since Node's global fetch and agents apply the
|
|
24
|
+
* environment's proxy under `NODE_USE_ENV_PROXY`. A proxy URL that cannot
|
|
25
|
+
* be used fails with `ProxyInvalid`; a runtime that is neither Node nor Bun
|
|
26
|
+
* fails with `ProxyUnsupported` when a proxy applies. Its connection pools
|
|
27
|
+
* close with the scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare const modelsTransportFor: (options: HttpProxyOptions) => Effect.Effect<Option.Option<Context.Service.Shape<typeof ModelsTransport>>, ProxyInvalid | ProxyUnsupported | Config.ConfigError, Scope.Scope>;
|
|
30
|
+
/**
|
|
31
|
+
* `ModelsTransportLive(options)` — the models plugin's network path from a
|
|
32
|
+
* proxy setting: `options.proxy` (or `https_proxy` / `HTTPS_PROXY` and
|
|
33
|
+
* `http_proxy` / `HTTP_PROXY` from the environment) for every call, except
|
|
34
|
+
* loopback hosts and the `noProxy` / `NO_PROXY` hosts, which go direct (see
|
|
35
|
+
* `resolveProxy`). With no proxy at all it is the global fetch, unchanged;
|
|
36
|
+
* with `noProxy: "*"` on Node, undici's direct agent. A proxy URL that cannot
|
|
37
|
+
* be used fails with `ProxyInvalid`; a runtime that is neither Node nor Bun
|
|
38
|
+
* fails with `ProxyUnsupported` when a proxy applies. Its connection pools
|
|
39
|
+
* close with the layer's scope. Hosts use it for the OAuth login helpers too:
|
|
40
|
+
* `exchangeAnthropicCode(code, verifier, { fetch: transport.fetch })`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const ModelsTransportLive: (options?: HttpProxyOptions) => Layer.Layer<ModelsTransport, ProxyInvalid | ProxyUnsupported | Config.ConfigError>;
|
|
43
|
+
/**
|
|
44
|
+
* A transport's fetch as `FetchHttpClient.Fetch` takes it (the type of the
|
|
45
|
+
* global `fetch`): a `Request` input is sent as its URL, method, headers,
|
|
46
|
+
* body and signal; Bun's `fetch.preconnect` does nothing.
|
|
47
|
+
*/
|
|
48
|
+
export declare const transportFetch: (transport: Context.Service.Shape<typeof ModelsTransport>) => typeof globalThis.fetch;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Agent as HttpAgent } from "node:http";
|
|
2
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
3
|
+
import { Config, Effect, Layer, Match, Option, Redacted } from "effect";
|
|
4
|
+
import { ModelsTransport } from "../ports/models-transport.port.js";
|
|
5
|
+
import { BUN_WEBSOCKET_PROXY_VERSION, NODE_WEBSOCKET_PROXY_VERSION, PROXY_VARIABLES, ProxyUnsupported } from "./http-proxy.entity.js";
|
|
6
|
+
import { atLeastVersion, displayProxy, proxyFor, redactCredentials, resolveProxy, turnsProxyOff, usesProxy } from "./http-proxy.entity.functions.js";
|
|
7
|
+
import { undiciFetch } from "./undici-fetch.adapter.js";
|
|
8
|
+
export const currentRuntime = () => {
|
|
9
|
+
const versions = typeof process > "u" ? {} : process.versions, bun = versions.bun, node = versions.node;
|
|
10
|
+
return bun !== void 0 ? { _tag: "Bun", version: bun } : node !== void 0 ? { _tag: "Node", version: node } : { _tag: "Other" };
|
|
11
|
+
};
|
|
12
|
+
const readEnvironment = Effect.forEach(PROXY_VARIABLES, (name) => Config.option(Config.String(name)).pipe(Effect.map((value) => Option.toArray(Option.map(value, (text) => [name, text]))))).pipe(Effect.map((entries) => Object.fromEntries(entries.flat()))), targetOf = (input) => typeof input === "string" ? URL.canParse(input) ? Option.some(new URL(input)) : Option.none() : Option.some(input), isSecure = (target) => target.protocol === "https:" || target.protocol === "wss:", describeFailure = (cause) => {
|
|
13
|
+
const own = cause instanceof Error ? cause.message : String(cause), inner = cause instanceof Error && cause.cause !== void 0 ? cause.cause instanceof Error ? cause.cause.message : String(cause.cause) : "";
|
|
14
|
+
return redactCredentials(inner.length > 0 && inner !== own ? `${own}: ${inner}` : own);
|
|
15
|
+
}, throughProxy = (target, proxy, request) => request.then((response) => response, (cause) => Promise.reject(cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError") ? cause : TypeError(`the request to ${target.protocol}//${target.host} through the proxy ${displayProxy(proxy)} failed: ${describeFailure(cause)}`, { cause }))), directTransport = ModelsTransport.of({ fetch: (input, init) => globalThis.fetch(input, init) }), webSocketRefused = (target, why) => new ProxyUnsupported({
|
|
16
|
+
message: `${why}, so the WebSocket to ${target.host} is refused instead of connecting directly past the proxy`
|
|
17
|
+
}), refuseProxyAuthentication = (response) => response.status === 407 ? (response.body === null ? Promise.resolve() : response.body.cancel().then(() => {
|
|
18
|
+
return;
|
|
19
|
+
}, () => {
|
|
20
|
+
return;
|
|
21
|
+
})).then(() => Promise.reject(Error("the proxy refused its credentials (407 Proxy Authentication Required)"))) : Promise.resolve(response), bunTransport = (config, version) => ModelsTransport.of({
|
|
22
|
+
fetch: (input, init) => Option.match(Option.flatMap(targetOf(input), (target) => Option.map(proxyFor(target, config), (proxy) => [target, proxy])), {
|
|
23
|
+
onNone: () => globalThis.fetch(input, init),
|
|
24
|
+
onSome: ([target, proxy]) => throughProxy(target, proxy, globalThis.fetch(input, { ...init, proxy: Redacted.value(proxy) }).then(refuseProxyAuthentication))
|
|
25
|
+
}),
|
|
26
|
+
webSocketAgent: (target) => Option.match(proxyFor(target, config), {
|
|
27
|
+
onNone: () => Effect.succeedNone,
|
|
28
|
+
onSome: (url) => atLeastVersion(version, BUN_WEBSOCKET_PROXY_VERSION) ? Effect.succeedSome({ _tag: "Proxy", url }) : Effect.fail(webSocketRefused(target, `Bun ${version} cannot send a WebSocket through a proxy (Bun ${BUN_WEBSOCKET_PROXY_VERSION} and later can)`))
|
|
29
|
+
})
|
|
30
|
+
}), isConnector = (value) => typeof value === "function", codeOf = (error) => ("code" in error) ? error.code : void 0, tunnelOnce = (connect, proxy) => (options, callback) => connect(options, (...result) => {
|
|
31
|
+
const [error] = result;
|
|
32
|
+
return error !== null && codeOf(error) === "UND_ERR_SOCKET" ? callback(Error(`the proxy ${displayProxy(proxy)} closed the connection while opening a tunnel to ${options.hostname}:${options.port.length > 0 ? options.port : options.protocol === "https:" ? "443" : "80"} (${error.message})`, { cause: error }), null) : callback(...result);
|
|
33
|
+
}), loadUndici = Effect.tryPromise({
|
|
34
|
+
try: () => import("undici"),
|
|
35
|
+
catch: (cause) => new ProxyUnsupported({ message: `undici, which sends the models plugin's calls on Node under a proxy setting, could not be loaded: ${describeFailure(cause)}` })
|
|
36
|
+
}), nodeDirect = (undici) => Effect.gen(function* () {
|
|
37
|
+
const dispatcher = new undici.Agent, sockets = { https: new HttpsAgent, http: new HttpAgent };
|
|
38
|
+
yield* Effect.addFinalizer(() => Effect.all([
|
|
39
|
+
Effect.promise(() => dispatcher.destroy().then(() => {
|
|
40
|
+
return;
|
|
41
|
+
}, () => {
|
|
42
|
+
return;
|
|
43
|
+
})),
|
|
44
|
+
Effect.sync(() => [sockets.https, sockets.http].forEach((agent) => agent.destroy()))
|
|
45
|
+
], { discard: !0 }));
|
|
46
|
+
return {
|
|
47
|
+
fetch: undiciFetch(undici.fetch, dispatcher),
|
|
48
|
+
webSocket: (target) => ({ _tag: "Agent", agent: isSecure(target) ? sockets.https : sockets.http })
|
|
49
|
+
};
|
|
50
|
+
}), nodeDirectTransport = Effect.gen(function* () {
|
|
51
|
+
const direct = yield* nodeDirect(yield* loadUndici);
|
|
52
|
+
return ModelsTransport.of({ fetch: direct.fetch, webSocketAgent: (target) => Effect.succeedSome(direct.webSocket(target)) });
|
|
53
|
+
}), nodeTransport = (config, version) => Effect.gen(function* () {
|
|
54
|
+
const undici = yield* loadUndici, direct = yield* nodeDirect(undici), proxyAgent = (proxy) => new undici.ProxyAgent({
|
|
55
|
+
uri: Redacted.value(proxy),
|
|
56
|
+
factory: (origin, options) => {
|
|
57
|
+
const connect = Reflect.get(options, "connect");
|
|
58
|
+
return new undici.Pool(origin, { ...options, ...isConnector(connect) ? { connect: tunnelOnce(connect, proxy) } : {} });
|
|
59
|
+
}
|
|
60
|
+
}), sameUrl = Option.isSome(config.https) && Option.isSome(config.http) && Redacted.value(config.https.value) === Redacted.value(config.http.value), route = (proxy) => {
|
|
61
|
+
const dispatcher = proxyAgent(proxy);
|
|
62
|
+
return { proxy, dispatcher, fetch: undiciFetch(undici.fetch, dispatcher) };
|
|
63
|
+
}, httpsRoute = Option.map(config.https, route), httpRoute = sameUrl ? httpsRoute : Option.map(config.http, route), socketAgents = {
|
|
64
|
+
https: Option.map(config.https, (proxy) => new HttpsAgent({ proxyEnv: { HTTPS_PROXY: Redacted.value(proxy) } })),
|
|
65
|
+
http: Option.map(config.http, (proxy) => new HttpAgent({ proxyEnv: { HTTP_PROXY: Redacted.value(proxy) } }))
|
|
66
|
+
}, dispatchers = [...Option.toArray(Option.map(httpsRoute, (route) => route.dispatcher)), ...sameUrl ? [] : Option.toArray(Option.map(httpRoute, (route) => route.dispatcher))];
|
|
67
|
+
yield* Effect.addFinalizer(() => Effect.all([
|
|
68
|
+
Effect.forEach(dispatchers, (dispatcher) => Effect.promise(() => dispatcher.destroy().then(() => {
|
|
69
|
+
return;
|
|
70
|
+
}, () => {
|
|
71
|
+
return;
|
|
72
|
+
})), { discard: !0 }),
|
|
73
|
+
Effect.sync(() => [...Option.toArray(socketAgents.https), ...Option.toArray(socketAgents.http)].forEach((agent) => agent.destroy()))
|
|
74
|
+
], { discard: !0 }));
|
|
75
|
+
const routeFor = (target) => Option.flatMap(proxyFor(target, config), () => isSecure(target) ? httpsRoute : httpRoute), fetch = (input, init) => Option.match(Option.flatMap(targetOf(input), (target) => Option.map(routeFor(target), (route) => [target, route])), {
|
|
76
|
+
onNone: () => direct.fetch(input, init),
|
|
77
|
+
onSome: ([target, route]) => throughProxy(target, route.proxy, route.fetch(input, init))
|
|
78
|
+
});
|
|
79
|
+
return ModelsTransport.of({
|
|
80
|
+
fetch,
|
|
81
|
+
webSocketAgent: (target) => Option.match(Option.flatMap(proxyFor(target, config), () => isSecure(target) ? socketAgents.https : socketAgents.http), {
|
|
82
|
+
onNone: () => Effect.succeedSome(direct.webSocket(target)),
|
|
83
|
+
onSome: (agent) => atLeastVersion(version, NODE_WEBSOCKET_PROXY_VERSION) ? Effect.succeedSome({ _tag: "Agent", agent }) : Effect.fail(webSocketRefused(target, `Node ${version} ignores the proxyEnv option of https.Agent (Node ${NODE_WEBSOCKET_PROXY_VERSION} and later honour it)`))
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
export const modelsTransportFor = (options) => Effect.gen(function* () {
|
|
88
|
+
const config = yield* Effect.fromResult(resolveProxy(options, yield* readEnvironment)), runtime = currentRuntime();
|
|
89
|
+
if (!usesProxy(config))
|
|
90
|
+
return yield* Match.value(runtime).pipe(Match.tag("Node", () => turnsProxyOff(config) ? Effect.map(nodeDirectTransport, Option.some) : Effect.succeedNone), Match.orElse(() => Effect.succeedNone));
|
|
91
|
+
return Option.some(yield* Match.value(runtime).pipe(Match.tag("Bun", ({ version }) => Effect.succeed(bunTransport(config, version))), Match.tag("Node", ({ version }) => nodeTransport(config, version)), Match.tag("Other", () => Effect.fail(new ProxyUnsupported({
|
|
92
|
+
message: "a proxy is configured, but this runtime is neither Node nor Bun: the models plugin cannot send its calls through a proxy here"
|
|
93
|
+
}))), Match.exhaustive));
|
|
94
|
+
}), ModelsTransportLive = (options = {}) => Layer.effect(ModelsTransport, Effect.map(modelsTransportFor(options), Option.getOrElse(() => directTransport))), transportFetch = (transport) => Object.assign((input, init) => input instanceof Request ? input.arrayBuffer().then((body) => transport.fetch(input.url, {
|
|
95
|
+
method: input.method,
|
|
96
|
+
headers: input.headers,
|
|
97
|
+
body: body.byteLength === 0 ? null : body,
|
|
98
|
+
signal: input.signal,
|
|
99
|
+
...init
|
|
100
|
+
})) : transport.fetch(input, init), { preconnect: () => {
|
|
101
|
+
return;
|
|
102
|
+
} });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One request a `RecordingProxy` received: a CONNECT tunnel (`host:port`) or an absolute-form request (its URL). */
|
|
2
|
+
export interface ProxiedRequest {
|
|
3
|
+
readonly kind: "connect" | "absolute";
|
|
4
|
+
readonly target: string;
|
|
5
|
+
readonly authorization: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface RecordingProxy {
|
|
8
|
+
readonly url: string;
|
|
9
|
+
readonly port: number;
|
|
10
|
+
readonly seen: ReadonlyArray<ProxiedRequest>;
|
|
11
|
+
readonly close: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A recording HTTP proxy on 127.0.0.1 for tests, over raw sockets (so no
|
|
15
|
+
* environment proxy applies to it): it records every request, opens CONNECT
|
|
16
|
+
* tunnels and forwards absolute-form requests to `*.invalid` hosts on this
|
|
17
|
+
* machine (the port as asked), and refuses every other host with 403, so a
|
|
18
|
+
* test never reaches the network. `reset: true` makes it close every
|
|
19
|
+
* connection as soon as it opens; `authenticate: true` answers every request
|
|
20
|
+
* with 407 (Proxy Authentication Required).
|
|
21
|
+
*/
|
|
22
|
+
export declare const startRecordingProxy: (options?: {
|
|
23
|
+
readonly reset?: boolean;
|
|
24
|
+
readonly authenticate?: boolean;
|
|
25
|
+
}) => Promise<RecordingProxy>;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the machine running the tests has a proxy in its environment. Bun's
|
|
28
|
+
* fetch applies one to every request without a `proxy` option, whatever a
|
|
29
|
+
* test's `ConfigProvider` says, so the tests of direct requests skip then.
|
|
30
|
+
*/
|
|
31
|
+
export declare const ambientProxy: boolean;
|
|
32
|
+
/** A port on 127.0.0.1 that nothing listens on (bound, then released). */
|
|
33
|
+
export declare const closedPort: () => Promise<number>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Dispatcher } from "undici";
|
|
2
|
+
import type { ModelsFetch } from "../ports/models-transport.port.js";
|
|
3
|
+
/**
|
|
4
|
+
* undici's `fetch` through `dispatcher`, typed as the global `fetch`. The
|
|
5
|
+
* npm undici's requests and responses have the shape of the global fetch
|
|
6
|
+
* types, but are distinct classes (Node's own fetch is a bundled undici of
|
|
7
|
+
* its own), and undici declares its own `Headers`, `Blob` and streams. So
|
|
8
|
+
* nothing downstream may rely on `instanceof Response` or `Headers`: effect's
|
|
9
|
+
* `FetchHttpClient`, the OpenAI-compatible client and the OAuth requests
|
|
10
|
+
* read them by shape. This file
|
|
11
|
+
* is the type-erasure boundary between the two declarations.
|
|
12
|
+
*/
|
|
13
|
+
export declare const undiciFetch: (fetch: typeof import("undici").fetch, dispatcher: Dispatcher) => ModelsFetch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const undiciFetch = (fetch, dispatcher) => (input, init) => fetch(input, { ...init, dispatcher });
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/plugin-models",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=24.5",
|
|
6
|
+
"bun": ">=1.3.0"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"main": "./dist/index.js",
|
|
6
10
|
"exports": {
|
|
@@ -14,10 +18,11 @@
|
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"dependencies": {
|
|
17
|
-
"@xandreed/core": "0.
|
|
18
|
-
"effect": "4.0.
|
|
19
|
-
"@effect/ai-anthropic": "4.0.
|
|
20
|
-
"@effect/ai-openai": "4.0.
|
|
21
|
+
"@xandreed/core": "0.14.0",
|
|
22
|
+
"effect": "4.0.7",
|
|
23
|
+
"@effect/ai-anthropic": "4.0.7",
|
|
24
|
+
"@effect/ai-openai": "4.0.7",
|
|
25
|
+
"undici": "^7.30.1",
|
|
21
26
|
"ws": "^8.21.0"
|
|
22
27
|
},
|
|
23
28
|
"private": false,
|
|
@@ -29,9 +34,5 @@
|
|
|
29
34
|
"README.md",
|
|
30
35
|
"LICENSE"
|
|
31
36
|
],
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=24",
|
|
34
|
-
"bun": ">=1.3.0"
|
|
35
|
-
},
|
|
36
37
|
"license": "MIT"
|
|
37
38
|
}
|