@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
|
@@ -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,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,13 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import type { ConformanceCheck } from "@xandreed/core/conformance";
|
|
4
|
+
import type { ResilientHttpError, ResilientHttpResponse } from "./resilient-http-client.adapter.js";
|
|
5
|
+
import type { ProviderFailureProtocol } from "../llm/provider-failure.entity.js";
|
|
6
|
+
/** A fresh logical request over a supplied raw transport; request retries belong to the subject. @public */
|
|
7
|
+
export interface ModelRequestSubject {
|
|
8
|
+
readonly request: (client: HttpClient.HttpClient, request: HttpClientRequest.HttpClientRequest, options: {
|
|
9
|
+
readonly protocol: ProviderFailureProtocol;
|
|
10
|
+
}) => Effect.Effect<Pick<ResilientHttpResponse, "text" | "status">, ResilientHttpError>;
|
|
11
|
+
}
|
|
12
|
+
/** Bounded request-policy contract, independent of provider credentials or a real network. @public */
|
|
13
|
+
export declare const modelRequestConformance: (subject?: ModelRequestSubject) => ReadonlyArray<ConformanceCheck>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, Exit, Option, Ref } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
3
|
+
import { modelFailureOf } from "@xandreed/core";
|
|
4
|
+
import { check, ConformanceFailure } from "@xandreed/core/conformance";
|
|
5
|
+
import { modelRetryDefaults } from "../llm/model-retry-policy.entity.js";
|
|
6
|
+
import { CurrentModelRetryPolicy } from "../ports/model-retry-policy.port.js";
|
|
7
|
+
import { resilientRequest } from "./resilient-http-client.adapter.js";
|
|
8
|
+
const policy = { ...modelRetryDefaults, totalTimeoutMs: 2000, requestTimeoutMs: 100, bodyIdleTimeoutMs: 100, backoff: { initialMs: 0, maxMs: 0, jitter: 0 } }, request = HttpClientRequest.post("https://conformance.invalid/messages").pipe(HttpClientRequest.bodyJsonUnsafe({ fixture: !0 })), expect = (id, holds, message) => holds ? Effect.void : Effect.fail(new ConformanceFailure({ check: id, message })), response = (request, status, type) => HttpClientResponse.fromWeb(request, Response.json({ error: { type, message: "conformance fixture" } }, { status }));
|
|
9
|
+
export const modelRequestConformance = (subject = { request: resilientRequest }) => [
|
|
10
|
+
check("uses one counter for transport failures and rejected statuses", "mixed-attempts", () => Effect.gen(function* () {
|
|
11
|
+
const sends = yield* Ref.make(0), client = HttpClient.make((request) => Effect.flatMap(Ref.getAndUpdate(sends, (n) => n + 1), (ordinal) => ordinal === 0 ? Effect.fail(new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({ request, description: "fixture" }) })) : Effect.succeed(ordinal < 3 ? response(request, ordinal === 1 ? 429 : 529, ordinal === 1 ? "rate_limit_error" : "overloaded_error") : HttpClientResponse.fromWeb(request, new Response("ok"))))), result = yield* subject.request(client, request, { protocol: "anthropic" }).pipe(Effect.flatMap((reply) => reply.text), Effect.provideService(CurrentModelRetryPolicy, policy));
|
|
12
|
+
yield* expect("mixed-attempts", result === "ok" && (yield* Ref.get(sends)) === 4, "mixed failures did not reach one fourth-attempt success");
|
|
13
|
+
})),
|
|
14
|
+
check("classifies a structured permanent error before retrying its status", "permanent-status", () => Effect.gen(function* () {
|
|
15
|
+
const sends = yield* Ref.make(0), client = HttpClient.make((request) => Ref.update(sends, (n) => n + 1).pipe(Effect.as(response(request, 429, "invalid_request_error")))), result = yield* Effect.exit(subject.request(client, request, { protocol: "compat" }).pipe(Effect.provideService(CurrentModelRetryPolicy, policy)));
|
|
16
|
+
yield* expect("permanent-status", Exit.isFailure(result) && (yield* Ref.get(sends)) === 1 && Option.contains(modelFailureOf(result.cause.reasons.find((reason) => reason._tag === "Fail")?.error), "InvalidRequest"), "a permanent request error was retried or lost its semantic kind");
|
|
17
|
+
})),
|
|
18
|
+
check("reads a call's policy at execution and obeys its total attempt limit", "dynamic-policy", () => Effect.gen(function* () {
|
|
19
|
+
const run = (maxAttempts) => Effect.gen(function* () {
|
|
20
|
+
const sends = yield* Ref.make(0), client = HttpClient.make((request) => Ref.update(sends, (n) => n + 1).pipe(Effect.as(response(request, 503, "overloaded_error"))));
|
|
21
|
+
yield* Effect.exit(subject.request(client, request, { protocol: "anthropic" }).pipe(Effect.provideService(CurrentModelRetryPolicy, { ...policy, maxAttempts })));
|
|
22
|
+
return yield* Ref.get(sends);
|
|
23
|
+
}), one = yield* run(1), three = yield* run(3);
|
|
24
|
+
yield* expect("dynamic-policy", one === 1 && three === 3, "a call ignored its policy or repeated beyond its total attempt cap");
|
|
25
|
+
})),
|
|
26
|
+
check("never sends a settled request again after its pending headers time out", "settled-headers", () => Effect.gen(function* () {
|
|
27
|
+
const sends = yield* Ref.make(0), client = HttpClient.make(() => Ref.update(sends, (n) => n + 1).pipe(Effect.andThen(Effect.never))), result = yield* subject.request(client, request, { protocol: "anthropic" }).pipe(Effect.provideService(CurrentModelRetryPolicy, { ...policy, requestTimeoutMs: 50, totalTimeoutMs: 300 }), Effect.exit, Effect.timeoutOption("2 seconds")), timedOut = Option.exists(result, (exit) => Exit.isFailure(exit) && Option.contains(modelFailureOf(exit.cause.reasons.find((reason) => reason._tag === "Fail")?.error), "Timeout"));
|
|
28
|
+
yield* expect("settled-headers", timedOut && (yield* Ref.get(sends)) === 1, "a settled request was cut before its deadline and sent again, or never timed out");
|
|
29
|
+
})),
|
|
30
|
+
check("reads a successful body that loses its connection as a transient failure", "lost-body", () => Effect.gen(function* () {
|
|
31
|
+
const client = HttpClient.make((request) => Effect.sync(() => HttpClientResponse.fromWeb(request, new Response(new ReadableStream({ start(controller) {
|
|
32
|
+
controller.error(Error("conformance connection reset"));
|
|
33
|
+
} }))))), result = yield* subject.request(client, request, { protocol: "anthropic" }).pipe(Effect.flatMap((reply) => reply.text), Effect.exit, Effect.provideService(CurrentModelRetryPolicy, policy));
|
|
34
|
+
yield* expect("lost-body", Exit.isFailure(result) && Option.contains(modelFailureOf(result.cause.reasons.find((reason) => reason._tag === "Fail")?.error), "Transient"), "a lost connection after handoff became a permanent or unclassified failure");
|
|
35
|
+
}))
|
|
36
|
+
];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer, Option, Redacted } from "effect";
|
|
2
|
+
import type { Scope } from "effect";
|
|
3
|
+
import { ModelsTransport } from "../ports/models-transport.port.js";
|
|
4
|
+
import type { ModelsFetch, WebSocketProxy } from "../ports/models-transport.port.js";
|
|
5
|
+
import { ProxyUnsupported } from "./http-proxy.entity.js";
|
|
6
|
+
import type { HttpProxyOptions, ProxyInvalid } from "./http-proxy.entity.js";
|
|
7
|
+
/** The runtime the plugin runs on, told apart without a Bun-only import (the subscription's WebSocket checks its route against it too). */
|
|
8
|
+
export type Runtime = {
|
|
9
|
+
readonly _tag: "Bun";
|
|
10
|
+
readonly version: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly _tag: "Node";
|
|
13
|
+
readonly version: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly _tag: "Other";
|
|
16
|
+
};
|
|
17
|
+
export declare const currentRuntime: () => Runtime;
|
|
18
|
+
export declare const modelsTransportContextFor: (options: {
|
|
19
|
+
readonly proxy?: string | Redacted.Redacted<string>;
|
|
20
|
+
readonly noProxy?: string;
|
|
21
|
+
}) => Effect.Effect<Context.Context<never>, ProxyInvalid | ProxyUnsupported | Config.ConfigError, Scope.Scope>;
|
|
22
|
+
/** The scoped transport: a proxy's, or the managed direct one on Node and Bun; none on another runtime without a proxy. */
|
|
23
|
+
export declare const modelsTransportFor: (options: {
|
|
24
|
+
readonly proxy?: string | Redacted.Redacted<string>;
|
|
25
|
+
readonly noProxy?: string;
|
|
26
|
+
}) => Effect.Effect<Option.Option<{
|
|
27
|
+
readonly fetch: ModelsFetch;
|
|
28
|
+
readonly webSocketAgent?: (target: URL) => Effect.Effect<Option.Option<WebSocketProxy>, ProxyUnsupported>;
|
|
29
|
+
}>, 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`). Node always uses a scoped direct undici Agent with its
|
|
36
|
+
* header/body timeouts disabled; Bun uses its native fetch with fetch's own
|
|
37
|
+
* timeout turned off. A proxy URL that cannot
|
|
38
|
+
* be used fails with `ProxyInvalid`; a runtime that is neither Node nor Bun
|
|
39
|
+
* fails with `ProxyUnsupported` when a proxy applies. Its connection pools
|
|
40
|
+
* close with the layer's scope. Hosts use it for the OAuth login helpers too:
|
|
41
|
+
* `exchangeAnthropicCode(code, verifier, { fetch: transport.fetch })`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ModelsTransportLive: (options?: HttpProxyOptions) => Layer.Layer<ModelsTransport, ProxyInvalid | ProxyUnsupported | Config.ConfigError>;
|
|
44
|
+
/**
|
|
45
|
+
* A transport's fetch as `FetchHttpClient.Fetch` takes it (the type of the
|
|
46
|
+
* global `fetch`): a `Request` input is sent as its URL, method, headers,
|
|
47
|
+
* body and signal; Bun's `fetch.preconnect` does nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare const transportFetch: (transport: Context.Service.Shape<typeof ModelsTransport>) => typeof globalThis.fetch;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Agent as HttpAgent } from "node:http";
|
|
2
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
3
|
+
import { Config, Context, Effect, Layer, Match, Option, Redacted } from "effect";
|
|
4
|
+
import { errorCodeOf } from "@xandreed/core";
|
|
5
|
+
import { CurrentManagedModelsTransport, ModelsTransport } from "../ports/models-transport.port.js";
|
|
6
|
+
import { BUN_WEBSOCKET_PROXY_VERSION, NODE_WEBSOCKET_PROXY_VERSION, PROXY_VARIABLES, ProxyUnsupported } from "./http-proxy.entity.js";
|
|
7
|
+
import { atLeastVersion, displayProxy, proxyFor, redactCredentials, resolveProxy, usesProxy } from "./http-proxy.entity.functions.js";
|
|
8
|
+
import { undiciFetch } from "./undici-fetch.adapter.js";
|
|
9
|
+
export const currentRuntime = () => {
|
|
10
|
+
const versions = typeof process > "u" ? {} : process.versions, bun = versions.bun, node = versions.node;
|
|
11
|
+
return bun !== void 0 ? { _tag: "Bun", version: bun } : node !== void 0 ? { _tag: "Node", version: node } : { _tag: "Other" };
|
|
12
|
+
};
|
|
13
|
+
const readEnvironment = Effect.forEach(PROXY_VARIABLES, (name) => Config.option(Config.String(name)).pipe(Effect.map((value) => Option.toArray(Option.map(value, (text) => [name, text])))), { concurrency: 1 }).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) => {
|
|
14
|
+
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) : "";
|
|
15
|
+
return redactCredentials(inner.length > 0 && inner !== own ? `${own}: ${inner}` : own);
|
|
16
|
+
}, 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) }), untimed = (init) => ({ ...init, timeout: !1 }), bunDirectTransport = ModelsTransport.of({ fetch: (input, init) => globalThis.fetch(input, untimed(init)) }), webSocketRefused = (target, why) => new ProxyUnsupported({
|
|
17
|
+
message: `${why}, so the WebSocket to ${target.host} is refused instead of connecting directly past the proxy`
|
|
18
|
+
}), refuseProxyAuthentication = (response) => response.status === 407 ? (response.body === null ? Promise.resolve() : response.body.cancel().then(() => {
|
|
19
|
+
return;
|
|
20
|
+
}, () => {
|
|
21
|
+
return;
|
|
22
|
+
})).then(() => Promise.reject(Error("the proxy refused its credentials (407 Proxy Authentication Required)"))) : Promise.resolve(response), bunTransport = (config, version) => ModelsTransport.of({
|
|
23
|
+
fetch: (input, init) => Option.match(Option.flatMap(targetOf(input), (target) => Option.map(proxyFor(target, config), (proxy) => [target, proxy])), {
|
|
24
|
+
onNone: () => globalThis.fetch(input, untimed(init)),
|
|
25
|
+
onSome: ([target, proxy]) => throughProxy(target, proxy, globalThis.fetch(input, { ...untimed(init), proxy: Redacted.value(proxy) }).then(refuseProxyAuthentication))
|
|
26
|
+
}),
|
|
27
|
+
webSocketAgent: (target) => Option.match(proxyFor(target, config), {
|
|
28
|
+
onNone: () => Effect.succeedNone,
|
|
29
|
+
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)`))
|
|
30
|
+
})
|
|
31
|
+
}), isConnector = (value) => typeof value === "function", tunnelOnce = (connect, proxy) => (options, callback) => connect(options, (...result) => {
|
|
32
|
+
const [error] = result;
|
|
33
|
+
return error !== null && Option.contains(errorCodeOf(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);
|
|
34
|
+
}), loadUndici = Effect.tryPromise({
|
|
35
|
+
try: () => import("undici"),
|
|
36
|
+
catch: (cause) => new ProxyUnsupported({ message: `undici, which sends the models plugin's calls on Node, could not be loaded: ${describeFailure(cause)}` })
|
|
37
|
+
}), destroyDispatcher = (dispatcher) => Effect.tryPromise({ try: () => dispatcher.destroy(), catch: (cause) => cause }).pipe(Effect.catch((cause) => Effect.logDebug("models transport: a connection pool did not close", cause))), nodeDirect = (undici) => Effect.gen(function* () {
|
|
38
|
+
const dispatcher = new undici.Agent({ headersTimeout: 0, bodyTimeout: 0 }), sockets = { https: new HttpsAgent, http: new HttpAgent };
|
|
39
|
+
yield* Effect.addFinalizer(() => Effect.all([
|
|
40
|
+
destroyDispatcher(dispatcher),
|
|
41
|
+
Effect.sync(() => [sockets.https, sockets.http].forEach((agent) => agent.destroy()))
|
|
42
|
+
], { concurrency: 1, discard: !0 }));
|
|
43
|
+
return {
|
|
44
|
+
fetch: undiciFetch(undici.fetch, dispatcher),
|
|
45
|
+
webSocket: (target) => ({ _tag: "Agent", agent: isSecure(target) ? sockets.https : sockets.http })
|
|
46
|
+
};
|
|
47
|
+
}), nodeDirectTransport = Effect.gen(function* () {
|
|
48
|
+
const direct = yield* nodeDirect(yield* loadUndici);
|
|
49
|
+
return ModelsTransport.of({ fetch: direct.fetch, webSocketAgent: (target) => Effect.succeedSome(direct.webSocket(target)) });
|
|
50
|
+
}), nodeTransport = (config, version) => Effect.gen(function* () {
|
|
51
|
+
const undici = yield* loadUndici, direct = yield* nodeDirect(undici), proxyAgent = (proxy) => new undici.ProxyAgent({
|
|
52
|
+
uri: Redacted.value(proxy),
|
|
53
|
+
clientFactory: (origin, options) => new undici.Pool(origin, { ...options, headersTimeout: 0, bodyTimeout: 0 }),
|
|
54
|
+
factory: (origin, options) => {
|
|
55
|
+
const connect = Reflect.get(options, "connect");
|
|
56
|
+
return new undici.Pool(origin, { ...options, ...isConnector(connect) ? { connect: tunnelOnce(connect, proxy) } : {}, headersTimeout: 0, bodyTimeout: 0 });
|
|
57
|
+
}
|
|
58
|
+
}), sameUrl = Option.isSome(config.https) && Option.isSome(config.http) && Redacted.value(config.https.value) === Redacted.value(config.http.value), route = (proxy) => {
|
|
59
|
+
const dispatcher = proxyAgent(proxy);
|
|
60
|
+
return { proxy, dispatcher, fetch: undiciFetch(undici.fetch, dispatcher) };
|
|
61
|
+
}, httpsRoute = Option.map(config.https, route), httpRoute = sameUrl ? httpsRoute : Option.map(config.http, route), socketAgents = {
|
|
62
|
+
https: Option.map(config.https, (proxy) => new HttpsAgent({ proxyEnv: { HTTPS_PROXY: Redacted.value(proxy) } })),
|
|
63
|
+
http: Option.map(config.http, (proxy) => new HttpAgent({ proxyEnv: { HTTP_PROXY: Redacted.value(proxy) } }))
|
|
64
|
+
}, dispatchers = [...Option.toArray(Option.map(httpsRoute, (route) => route.dispatcher)), ...sameUrl ? [] : Option.toArray(Option.map(httpRoute, (route) => route.dispatcher))];
|
|
65
|
+
yield* Effect.addFinalizer(() => Effect.all([
|
|
66
|
+
Effect.forEach(dispatchers, destroyDispatcher, { concurrency: 1, discard: !0 }),
|
|
67
|
+
Effect.sync(() => [...Option.toArray(socketAgents.https), ...Option.toArray(socketAgents.http)].forEach((agent) => agent.destroy()))
|
|
68
|
+
], { concurrency: 1, discard: !0 }));
|
|
69
|
+
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])), {
|
|
70
|
+
onNone: () => direct.fetch(input, init),
|
|
71
|
+
onSome: ([target, route]) => throughProxy(target, route.proxy, route.fetch(input, init))
|
|
72
|
+
});
|
|
73
|
+
return ModelsTransport.of({
|
|
74
|
+
fetch,
|
|
75
|
+
webSocketAgent: (target) => Option.match(Option.flatMap(proxyFor(target, config), () => isSecure(target) ? socketAgents.https : socketAgents.http), {
|
|
76
|
+
onNone: () => Effect.succeedSome(direct.webSocket(target)),
|
|
77
|
+
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)`))
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
}), managedDirect = (transport) => Context.make(ModelsTransport, transport).pipe(Context.add(CurrentManagedModelsTransport, Option.some(transport)));
|
|
81
|
+
export const modelsTransportContextFor = Effect.fnUntraced(function* (options) {
|
|
82
|
+
const config = yield* Effect.fromResult(resolveProxy(options, yield* readEnvironment)), runtime = currentRuntime();
|
|
83
|
+
if (!usesProxy(config))
|
|
84
|
+
return yield* Match.value(runtime).pipe(Match.tag("Node", () => Effect.map(nodeDirectTransport, managedDirect)), Match.tag("Bun", () => Effect.succeed(managedDirect(bunDirectTransport))), Match.orElse(() => Effect.succeed(Context.empty())));
|
|
85
|
+
const transport = 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({
|
|
86
|
+
message: "a proxy is configured, but this runtime is neither Node nor Bun: the models plugin cannot send its calls through a proxy here"
|
|
87
|
+
}))), Match.exhaustive);
|
|
88
|
+
return Context.make(ModelsTransport, transport).pipe(Context.add(CurrentManagedModelsTransport, Option.none()));
|
|
89
|
+
}), modelsTransportFor = Effect.fnUntraced(function* (options) {
|
|
90
|
+
return Context.getOption(yield* modelsTransportContextFor(options), ModelsTransport);
|
|
91
|
+
}), ModelsTransportLive = (options = {}) => Layer.effectContext(Effect.map(modelsTransportContextFor(options), (context) => Context.add(context, ModelsTransport, Option.getOrElse(Context.getOption(context, ModelsTransport), () => directTransport)))), transportFetch = (transport) => Object.assign((input, init) => input instanceof Request ? input.arrayBuffer().then((body) => transport.fetch(input.url, {
|
|
92
|
+
method: input.method,
|
|
93
|
+
headers: input.headers,
|
|
94
|
+
body: body.byteLength === 0 ? null : body,
|
|
95
|
+
signal: input.signal,
|
|
96
|
+
...init
|
|
97
|
+
})) : transport.fetch(input, init), { preconnect: () => {
|
|
98
|
+
return;
|
|
99
|
+
} });
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Effect, Ref, Schema, Stream } from "effect";
|
|
2
|
+
import { AiError } from "effect/ai";
|
|
3
|
+
import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/http";
|
|
4
|
+
import type { HttpClientRequest } from "effect/http";
|
|
5
|
+
import type { ModelCallMethod, ModelSelection, TurnError } from "@xandreed/core";
|
|
6
|
+
import { ModelRequestState, ModelRetryPolicy } from "../llm/model-retry-policy.entity.js";
|
|
7
|
+
import type { ProviderFailureProtocol } from "../llm/provider-failure.entity.js";
|
|
8
|
+
import type { ModelAttempt, ModelCompletionCall } from "../llm/model-completion.adapter.js";
|
|
9
|
+
export type ResilientHttpError = HttpClientError.HttpClientError | AiError.AiError | TurnError;
|
|
10
|
+
type BodyReaders = "json" | "text" | "arrayBuffer" | "urlParamsBody" | "formData" | "stream";
|
|
11
|
+
/** Exact body-reader error channels before the existing SDK-boundary adaptation. */
|
|
12
|
+
export type ResilientHttpResponse = Omit<HttpClientResponse.HttpClientResponse, BodyReaders> & {
|
|
13
|
+
readonly json: Effect.Effect<Schema.Json, ResilientHttpError>;
|
|
14
|
+
readonly text: Effect.Effect<string, ResilientHttpError>;
|
|
15
|
+
readonly arrayBuffer: Effect.Effect<ArrayBuffer, ResilientHttpError>;
|
|
16
|
+
readonly urlParamsBody: Effect.Effect<UrlParams.UrlParams, ResilientHttpError>;
|
|
17
|
+
readonly formData: Effect.Effect<FormData, ResilientHttpError>;
|
|
18
|
+
readonly stream: Stream.Stream<Uint8Array, ResilientHttpError>;
|
|
19
|
+
readonly modelAttempt: {
|
|
20
|
+
readonly call: ModelRequestCall;
|
|
21
|
+
readonly ordinal: number;
|
|
22
|
+
readonly completion: ModelAttempt;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** One call's ephemeral identity and observable physical-attempt state; no stored ledger. */
|
|
26
|
+
export interface ModelRequestCall {
|
|
27
|
+
readonly policy: ModelRetryPolicy;
|
|
28
|
+
readonly completion: ModelCompletionCall;
|
|
29
|
+
readonly startedAt: number;
|
|
30
|
+
readonly deadline: number;
|
|
31
|
+
readonly reserveFallback: boolean;
|
|
32
|
+
readonly state: Ref.Ref<ModelRequestState>;
|
|
33
|
+
}
|
|
34
|
+
export declare const makeModelRequestCall: (options: {
|
|
35
|
+
readonly reserveFallback: boolean;
|
|
36
|
+
}) => Effect.Effect<ModelRequestCall, AiError.AiError, never>;
|
|
37
|
+
export declare const modelRequestSnapshot: (call: ModelRequestCall) => Effect.Effect<ModelRequestState>;
|
|
38
|
+
export declare const enterModelFallback: (call: ModelRequestCall, error: unknown) => Effect.Effect<boolean, never, never>;
|
|
39
|
+
/**
|
|
40
|
+
* A handed-off response with its body observed as it passes. A response
|
|
41
|
+
* this client handed off stays guarded: its readers keep their exact typed
|
|
42
|
+
* failures (timeouts, lost connections) and its model attempt. Any other
|
|
43
|
+
* response is rebuilt over a Web body.
|
|
44
|
+
*/
|
|
45
|
+
export declare const observedResponse: (response: HttpClientResponse.HttpClientResponse, observe: (bytes: Uint8Array) => Effect.Effect<void>) => Effect.Effect<HttpClientResponse.HttpClientResponse | ResilientHttpResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* How one request is sent: its error vocabulary, whether its response is
|
|
48
|
+
* settled (headers arrive only once the whole response is generated; absent:
|
|
49
|
+
* every call method but a stream's is settled) and the caller's admission
|
|
50
|
+
* before each physical send.
|
|
51
|
+
*/
|
|
52
|
+
export interface ResilientRequestOptions {
|
|
53
|
+
readonly protocol: ProviderFailureProtocol;
|
|
54
|
+
readonly settled?: boolean;
|
|
55
|
+
readonly beforeSend?: Effect.Effect<void, AiError.AiError>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One retry owner before response handoff; successful response readers
|
|
59
|
+
* retain exact typed failures. A streamed request waits for its headers at
|
|
60
|
+
* most `requestTimeoutMs`; a settled one's headers come with the whole
|
|
61
|
+
* generation, so they wait for the call's deadline and a timeout there is
|
|
62
|
+
* final instead of a second generation.
|
|
63
|
+
*/
|
|
64
|
+
export declare const resilientRequest: <R>(client: HttpClient.HttpClient.With<HttpClientError.HttpClientError, R>, request: HttpClientRequest.HttpClientRequest, options: ResilientRequestOptions) => Effect.Effect<ResilientHttpResponse, ResilientHttpError, R>;
|
|
65
|
+
export declare const resilientHttpClient: <R>(client: HttpClient.HttpClient.With<HttpClientError.HttpClientError, R>, options: {
|
|
66
|
+
readonly protocol: ProviderFailureProtocol;
|
|
67
|
+
readonly adaptResponse: (response: ResilientHttpResponse) => HttpClientResponse.HttpClientResponse;
|
|
68
|
+
}) => HttpClient.HttpClient.With<ResilientHttpError, R>;
|
|
69
|
+
/** One provider invocation owns cleanup of a handed-off body that failed before raw decoding. */
|
|
70
|
+
export declare const withModelRequestCall: <A, E, R>(method: ModelCallMethod, selection: ModelSelection, operation: Effect.Effect<A, E, R>) => Effect.Effect<A, E | AiError.AiError | TurnError, R>;
|
|
71
|
+
/** Fixed SDK streams bridge an original recorder TurnError as a defect; no data/error relabeling. */
|
|
72
|
+
export declare const withModelRequestStream: <A, E, R>(method: ModelCallMethod, selection: ModelSelection, stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E | AiError.AiError | TurnError, R>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Clock, Duration, Effect, Exit, Inspectable, Match, Option, Random, Ref, Schema, Stream } from "effect";
|
|
2
|
+
import { AiError } from "effect/ai";
|
|
3
|
+
import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/http";
|
|
4
|
+
import { isTurnError, modelFailureMessageOf, modelFailureOf, stampAiErrorFailure } from "@xandreed/core";
|
|
5
|
+
import { ModelRequestState, ModelRetryPolicy } from "../llm/model-retry-policy.entity.js";
|
|
6
|
+
import { enterFallbackState, retryableModelFailure, retryDelayMillis, retryHintMillis, startAttemptState } from "../llm/model-retry-policy.entity.functions.js";
|
|
7
|
+
import { CurrentModelRequestCall, CurrentModelRetryPolicy } from "../ports/model-retry-policy.port.js";
|
|
8
|
+
import { ProviderErrorBody } from "../llm/provider-failure.entity.js";
|
|
9
|
+
import { classifyAiError } from "../llm/provider-failure.entity.functions.js";
|
|
10
|
+
import { classifyLlmError } from "../llm/retry.js";
|
|
11
|
+
import { attachModelAttempt, beginModelAttempt, completeModelAttempt, completePendingAttempts, makeCompletionCall, observeModelBody, retainModelAttempt } from "../llm/model-completion.adapter.js";
|
|
12
|
+
import { CurrentModelCallMetadataOwner, CurrentModelCallMethod, CurrentModelCallSelection } from "../ports/model-call-completion.port.js";
|
|
13
|
+
const timeoutFailure = () => stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.UnknownError({ description: modelFailureMessageOf("Timeout") }) }), "Timeout");
|
|
14
|
+
export const makeModelRequestCall = Effect.fnUntraced(function* (options) {
|
|
15
|
+
const policy = yield* Schema.decodeUnknownEffect(ModelRetryPolicy)(yield* CurrentModelRetryPolicy, { onExcessProperty: "error" }).pipe(Effect.mapError(() => stampAiErrorFailure(AiError.make({ module: "Models", method: "requestPolicy", reason: new AiError.InvalidRequestError({ description: modelFailureMessageOf("InvalidRequest") }) }), "InvalidRequest"))), startedAt = yield* Clock.currentTimeMillis, state = yield* Ref.make({ _tag: "Ready", rung: "Primary", attempts: 0 });
|
|
16
|
+
return { policy, completion: yield* makeCompletionCall(), startedAt, deadline: startedAt + policy.totalTimeoutMs, reserveFallback: options.reserveFallback, state };
|
|
17
|
+
}), modelRequestSnapshot = (call) => Ref.get(call.state), enterModelFallback = Effect.fnUntraced(function* (call, error) {
|
|
18
|
+
const now = yield* Clock.currentTimeMillis;
|
|
19
|
+
if (now >= call.deadline || classifyLlmError(error, now) !== "transient")
|
|
20
|
+
return !1;
|
|
21
|
+
return yield* Ref.modify(call.state, (state) => Option.match(enterFallbackState(state, call.policy, call.reserveFallback), {
|
|
22
|
+
onNone: () => [!1, state],
|
|
23
|
+
onSome: (next) => [!0, next]
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
const timed = (effect, boundMs) => effect.pipe(Effect.timeoutOrElse({ duration: Duration.millis(boundMs), orElse: () => Effect.fail(timeoutFailure()) })), pendingRead = (effect, call, boundMs) => Effect.flatMap(Clock.currentTimeMillis, (now) => now >= call.deadline ? Effect.fail(timeoutFailure()) : timed(effect, Math.min(boundMs, call.deadline - now))), rejectedBody = (response, call) => Stream.transformPull(response.stream, (pull) => Effect.succeed(pendingRead(pull, call, call.policy.bodyIdleTimeoutMs))), requestDetails = (request) => {
|
|
27
|
+
const url = URL.canParse(request.url) ? new URL(request.url) : new URL("https://provider.invalid/");
|
|
28
|
+
return { method: request.method, url: `${url.protocol}//${url.host}${url.pathname}`, urlParams: [], headers: {} };
|
|
29
|
+
}, bodyReadFailure = (response) => AiError.make({ module: "Models", method: "request", reason: new AiError.NetworkError({ reason: "TransportError", request: requestDetails(response.request), description: modelFailureMessageOf("Transient") }) }), committedBody = (response, call) => Stream.transformPull(response.stream, (pull) => Effect.succeed(timed(pull, call.policy.bodyIdleTimeoutMs))).pipe(Stream.mapError((error) => HttpClientError.isHttpClientError(error) && error.reason._tag === "DecodeError" ? bodyReadFailure(response) : error)), concatenate = (pieces) => {
|
|
30
|
+
const bytes = new Uint8Array(pieces.reduce((size, piece) => size + piece.byteLength, 0));
|
|
31
|
+
pieces.reduce((offset, piece) => {
|
|
32
|
+
bytes.set(piece, offset);
|
|
33
|
+
return offset + piece.byteLength;
|
|
34
|
+
}, 0);
|
|
35
|
+
return bytes;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class GuardedResponse {
|
|
39
|
+
stream;
|
|
40
|
+
buffered;
|
|
41
|
+
modelAttempt;
|
|
42
|
+
[HttpClientResponse.TypeId] = HttpClientResponse.TypeId;
|
|
43
|
+
["~effect/http/HttpIncomingMessage"] = "~effect/http/HttpIncomingMessage";
|
|
44
|
+
#original;
|
|
45
|
+
constructor(original, stream, buffered, modelAttempt) {
|
|
46
|
+
this.stream = stream;
|
|
47
|
+
this.buffered = buffered;
|
|
48
|
+
this.modelAttempt = modelAttempt;
|
|
49
|
+
this.#original = original;
|
|
50
|
+
}
|
|
51
|
+
get request() {
|
|
52
|
+
return this.#original.request;
|
|
53
|
+
}
|
|
54
|
+
get url() {
|
|
55
|
+
return this.#original.url;
|
|
56
|
+
}
|
|
57
|
+
get status() {
|
|
58
|
+
return this.#original.status;
|
|
59
|
+
}
|
|
60
|
+
get headers() {
|
|
61
|
+
return this.#original.headers;
|
|
62
|
+
}
|
|
63
|
+
get cookies() {
|
|
64
|
+
return this.#original.cookies;
|
|
65
|
+
}
|
|
66
|
+
get remoteAddress() {
|
|
67
|
+
return this.#original.remoteAddress;
|
|
68
|
+
}
|
|
69
|
+
get upgrade() {
|
|
70
|
+
return this.#original.upgrade;
|
|
71
|
+
}
|
|
72
|
+
get pipe() {
|
|
73
|
+
return this.#original.pipe;
|
|
74
|
+
}
|
|
75
|
+
get arrayBuffer() {
|
|
76
|
+
return Effect.map(this.buffered, (bytes) => bytes.buffer);
|
|
77
|
+
}
|
|
78
|
+
get text() {
|
|
79
|
+
return Effect.map(this.buffered, (bytes) => new TextDecoder().decode(bytes));
|
|
80
|
+
}
|
|
81
|
+
get json() {
|
|
82
|
+
return this.text.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))), Effect.mapError((cause) => AiError.isAiError(cause) || HttpClientError.isHttpClientError(cause) || isTurnError(cause) ? cause : this.decodeFailure(cause)));
|
|
83
|
+
}
|
|
84
|
+
get urlParamsBody() {
|
|
85
|
+
return Effect.map(this.text, (text) => UrlParams.fromInput(new URLSearchParams(text)));
|
|
86
|
+
}
|
|
87
|
+
get formData() {
|
|
88
|
+
return this.buffered.pipe(Effect.flatMap((bytes) => Effect.tryPromise({ try: () => new Response(bytes, { headers: this.headers }).formData(), catch: (cause) => this.decodeFailure(cause) })));
|
|
89
|
+
}
|
|
90
|
+
decodeFailure(cause) {
|
|
91
|
+
return new HttpClientError.HttpClientError({ reason: new HttpClientError.DecodeError({ request: this.request, response: this.#original, cause, description: modelFailureMessageOf("InvalidResponse") }) });
|
|
92
|
+
}
|
|
93
|
+
toJSON() {
|
|
94
|
+
return this.#original.toJSON();
|
|
95
|
+
}
|
|
96
|
+
toString() {
|
|
97
|
+
return this.#original.toString();
|
|
98
|
+
}
|
|
99
|
+
[Inspectable.NodeInspectSymbol]() {
|
|
100
|
+
return this.toJSON();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const guardedResponse = Effect.fnUntraced(function* (response, stream, modelAttempt) {
|
|
104
|
+
const buffered = yield* Effect.cached(Stream.runCollect(stream).pipe(Effect.map(concatenate)));
|
|
105
|
+
return attachModelAttempt(modelAttempt.completion, new GuardedResponse(response, stream, buffered, modelAttempt));
|
|
106
|
+
}), wrapResponse = (response, call, ordinal, completion) => guardedResponse(response, committedBody(response, call), { call, ordinal, completion });
|
|
107
|
+
export const observedResponse = (response, observe) => response instanceof GuardedResponse ? guardedResponse(response, response.stream.pipe(Stream.tap(observe)), response.modelAttempt) : Effect.map(Stream.toReadableStreamEffect(response.stream.pipe(Stream.tap(observe))), (body) => retainModelAttempt(response, HttpClientResponse.fromWeb(response.request, new Response(body, { status: response.status, headers: new Headers(Object.entries(response.headers)) }))));
|
|
108
|
+
const httpDetails = (response, body) => ({ request: requestDetails(response.request), response: { status: response.status, headers: Object.fromEntries(Object.entries(response.headers).filter(([key]) => ["request-id", "x-request-id", "retry-after", "retry-after-ms"].includes(key))) }, body }), responseFailure = (response, text, protocol) => {
|
|
109
|
+
const http = httpDetails(response, text), classified = classifyAiError(AiError.make({ module: "Models", method: "request", reason: new AiError.UnknownError({ http }) }), protocol), kind = Option.getOrElse(modelFailureOf(classified), () => "Unknown"), diagnostic = Option.flatMap(Schema.decodeUnknownOption(Schema.fromJsonString(ProviderErrorBody))(text), (body) => Option.fromNullishOr([
|
|
110
|
+
typeof body.error === "string" ? body.error : body.error?.message,
|
|
111
|
+
body.message
|
|
112
|
+
].find((value) => typeof value === "string" && value.trim().length > 0))), common = { http, description: Option.match(diagnostic, { onNone: () => modelFailureMessageOf(kind), onSome: (message) => message.slice(0, 500) }) }, reason = Match.value(kind).pipe(Match.when("Authentication", () => new AiError.AuthenticationError({ ...common, kind: response.status === 403 ? "InsufficientPermissions" : "InvalidKey" })), Match.when("Quota", () => new AiError.QuotaExhaustedError(common)), Match.when("RateLimit", () => new AiError.RateLimitError(common)), Match.when("InvalidRequest", () => new AiError.InvalidRequestError(common)), Match.when("ContextOverflow", () => new AiError.InvalidRequestError(common)), Match.when("InvalidResponse", () => new AiError.InvalidResponseError(common)), Match.when("Transient", () => new AiError.InternalProviderError(common)), Match.orElse(() => new AiError.UnknownError(common)));
|
|
113
|
+
return stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason }), kind);
|
|
114
|
+
}, errorBody = Effect.fnUntraced(function* (response, call, attempt) {
|
|
115
|
+
const bytes = new Uint8Array(65536), retained = yield* Ref.make(0), size = yield* Stream.runFoldEffect(rejectedBody(response, call), () => 0, (size, piece) => {
|
|
116
|
+
const keep = Math.min(piece.byteLength, bytes.byteLength - size);
|
|
117
|
+
bytes.set(piece.subarray(0, keep), size);
|
|
118
|
+
return size + piece.byteLength > bytes.byteLength ? Effect.fail(stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.InvalidResponseError({ description: modelFailureMessageOf("InvalidResponse"), http: httpDetails(response, new TextDecoder().decode(bytes)) }) }), "InvalidResponse")) : Ref.set(retained, size + piece.byteLength).pipe(Effect.as(size + piece.byteLength));
|
|
119
|
+
}).pipe(Effect.tap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !0)), Effect.catchTag("HttpClientError", (error) => ["DecodeError", "EmptyBodyError", "TransportError"].includes(error.reason._tag) ? Ref.get(retained).pipe(Effect.tap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !1))) : Effect.fail(error)), Effect.tapError(() => Ref.get(retained).pipe(Effect.flatMap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !1)))));
|
|
120
|
+
return new TextDecoder().decode(bytes.subarray(0, size));
|
|
121
|
+
}), errorKind = (error) => HttpClientError.isHttpClientError(error) ? Match.valueTags(error.reason, {
|
|
122
|
+
TransportError: () => "Transient",
|
|
123
|
+
EncodeError: () => "InvalidRequest",
|
|
124
|
+
InvalidUrlError: () => "InvalidRequest",
|
|
125
|
+
StatusCodeError: () => "Unknown",
|
|
126
|
+
DecodeError: () => "InvalidResponse",
|
|
127
|
+
EmptyBodyError: () => "InvalidResponse"
|
|
128
|
+
}) : Option.getOrElse(modelFailureOf(error), () => "Unknown"), replayableBody = (request) => Match.valueTags(request.body, {
|
|
129
|
+
Empty: () => !0,
|
|
130
|
+
Uint8Array: () => !0,
|
|
131
|
+
Stream: () => !1,
|
|
132
|
+
FormData: () => !1,
|
|
133
|
+
Raw: (body) => typeof body.body === "string" || body.body instanceof ArrayBuffer || ArrayBuffer.isView(body.body)
|
|
134
|
+
}), awaitReady = Effect.fnUntraced(function* (call) {
|
|
135
|
+
const now = yield* Clock.currentTimeMillis;
|
|
136
|
+
if (now >= call.deadline)
|
|
137
|
+
return yield* Effect.fail(timeoutFailure());
|
|
138
|
+
const state = yield* Ref.get(call.state), readyAt = ModelRequestState.match(state, { Ready: () => now, Rejected: (value) => value.retryAt ?? now, Sending: () => now, Committed: () => now });
|
|
139
|
+
if (readyAt > now)
|
|
140
|
+
yield* Effect.sleep(Duration.millis(Math.min(readyAt - now, call.deadline - now)));
|
|
141
|
+
if ((yield* Clock.currentTimeMillis) >= call.deadline)
|
|
142
|
+
return yield* Effect.fail(timeoutFailure());
|
|
143
|
+
}), requestCall = Effect.fnUntraced(function* () {
|
|
144
|
+
const inherited = yield* CurrentModelRequestCall;
|
|
145
|
+
if (Option.isSome(inherited) && (yield* Ref.get(inherited.value.state))._tag !== "Committed")
|
|
146
|
+
return inherited.value;
|
|
147
|
+
return yield* makeModelRequestCall({ reserveFallback: !1 });
|
|
148
|
+
});
|
|
149
|
+
export const resilientRequest = Effect.fn("models.request")(function* (client, request, options) {
|
|
150
|
+
const call = yield* requestCall(), replayable = replayableBody(request), headerBound = options.settled ?? (yield* CurrentModelCallMethod) !== "Stream" ? call.policy.totalTimeoutMs : call.policy.requestTimeoutMs, run = (previous) => Effect.gen(function* () {
|
|
151
|
+
yield* awaitReady(call);
|
|
152
|
+
yield* options.beforeSend ?? Effect.void;
|
|
153
|
+
const started = yield* Ref.modify(call.state, (state) => Option.match(startAttemptState(state, call.policy, call.reserveFallback), { onNone: () => [Option.none(), state], onSome: (next) => [Option.some(next.attempts), next] }));
|
|
154
|
+
if (Option.isNone(started))
|
|
155
|
+
return yield* Option.match(previous, { onNone: () => Effect.fail(stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.InvalidRequestError({ description: modelFailureMessageOf("InvalidRequest") }) }), "InvalidRequest")), onSome: Effect.fail });
|
|
156
|
+
const ordinal = started.value, attempt = yield* beginModelAttempt(call.completion, ordinal, options.protocol), execute = client.execute(request).pipe(Effect.provideService(CurrentModelRequestCall, Option.some(call)), Effect.catchTag("HttpClientError", (error) => error.reason._tag === "StatusCodeError" ? Effect.succeed(error.reason.response) : Effect.fail(error))), result = yield* Effect.result(pendingRead(execute, call, headerBound).pipe(Effect.flatMap((response) => response.status >= 200 && response.status < 300 ? Ref.update(call.state, (state) => state._tag === "Sending" && state.attempts === ordinal ? { _tag: "Committed", rung: state.rung, attempts: ordinal } : state).pipe(Effect.andThen(wrapResponse(response, call, ordinal, attempt))) : errorBody(response, call, attempt).pipe(Effect.flatMap((body) => Effect.fail(responseFailure(response, body, options.protocol)))))));
|
|
157
|
+
if (result._tag === "Success")
|
|
158
|
+
return result.success;
|
|
159
|
+
const failure = result.failure;
|
|
160
|
+
if (isTurnError(failure))
|
|
161
|
+
return yield* Effect.fail(failure);
|
|
162
|
+
const kind = errorKind(failure);
|
|
163
|
+
yield* completeModelAttempt(attempt, { _tag: "Failed", failure: Option.some(kind) });
|
|
164
|
+
const now = yield* Clock.currentTimeMillis, hint = AiError.isAiError(failure) && "http" in failure.reason ? retryHintMillis(failure.reason.http?.response?.headers ?? {}, now) : Option.none(), delay = retryDelayMillis(call.policy, ordinal, yield* Random.next, hint);
|
|
165
|
+
yield* Ref.update(call.state, (state) => state._tag === "Sending" && state.attempts === ordinal ? { _tag: "Rejected", rung: state.rung, attempts: ordinal, kind, replayable, ...retryableModelFailure(kind) && replayable ? { retryAt: now + delay } : {} } : state);
|
|
166
|
+
const state = yield* Ref.get(call.state);
|
|
167
|
+
return Option.isSome(startAttemptState(state, call.policy, call.reserveFallback)) ? yield* run(Option.some(failure)) : yield* Effect.fail(failure);
|
|
168
|
+
});
|
|
169
|
+
return yield* run(Option.none()).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? completePendingAttempts(call.completion, exit) : Effect.void));
|
|
170
|
+
}), resilientHttpClient = (client, options) => HttpClient.makeWith((prepared) => Effect.flatMap(prepared, (request) => resilientRequest(HttpClient.makeWith(client.postprocess, Effect.succeed), request, options).pipe(Effect.map(options.adaptResponse))), client.preprocess), withModelRequestCall = (method, selection, operation) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
171
|
+
const call = yield* requestCall(), inherited = Option.contains(yield* CurrentModelCallMetadataOwner, call.completion.callId), effectiveMethod = inherited ? yield* CurrentModelCallMethod : method, effectiveSelection = inherited ? yield* CurrentModelCallSelection : Option.some(selection), result = yield* Effect.exit(restore(operation.pipe(Effect.provideService(CurrentModelRequestCall, Option.some(call)), Effect.provideService(CurrentModelCallMethod, effectiveMethod), Effect.provideService(CurrentModelCallSelection, effectiveSelection), Effect.provideService(CurrentModelCallMetadataOwner, Option.some(call.completion.callId)))));
|
|
172
|
+
yield* completePendingAttempts(call.completion, result);
|
|
173
|
+
return yield* Exit.match(result, { onSuccess: Effect.succeed, onFailure: Effect.failCause });
|
|
174
|
+
})), withModelRequestStream = (method, selection, stream) => Stream.unwrap(Effect.gen(function* () {
|
|
175
|
+
const call = yield* requestCall(), inherited = Option.contains(yield* CurrentModelCallMetadataOwner, call.completion.callId), effectiveMethod = inherited ? yield* CurrentModelCallMethod : method, effectiveSelection = inherited ? yield* CurrentModelCallSelection : Option.some(selection);
|
|
176
|
+
return stream.pipe(Stream.provideService(CurrentModelRequestCall, Option.some(call)), Stream.provideService(CurrentModelCallMethod, effectiveMethod), Stream.provideService(CurrentModelCallSelection, effectiveSelection), Stream.provideService(CurrentModelCallMetadataOwner, Option.some(call.completion.callId)), Stream.onEnd(completePendingAttempts(call.completion, Exit.succeed(void 0))), Stream.onExit((exit) => completePendingAttempts(call.completion, Exit.isSuccess(exit) ? Exit.interrupt() : exit).pipe(Effect.catch((error) => Effect.die(error)))));
|
|
177
|
+
}));
|