@xandreed/plugin-models 0.14.0 → 0.15.1
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 +30 -10
- package/dist/auth/anthropicOAuth.js +15 -18
- 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 +23 -5
- package/dist/auth/credential-input.entity.functions.d.ts +15 -5
- package/dist/auth/credential-input.entity.functions.js +12 -9
- package/dist/auth/credential-input.entity.js +5 -3
- package/dist/auth/credential-refresh.entity.d.ts +11 -3
- package/dist/auth/credential-refresh.entity.functions.d.ts +48 -6
- package/dist/auth/credential-refresh.entity.functions.js +12 -3
- package/dist/auth/localAuth.d.ts +10 -1
- package/dist/auth/localAuth.js +53 -79
- package/dist/auth/memory-auth.adapter.d.ts +12 -4
- package/dist/auth/memory-auth.adapter.js +13 -25
- package/dist/auth/openAiCodexOAuth.d.ts +26 -5
- package/dist/auth/openAiCodexOAuth.js +17 -28
- 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 +19 -19
- package/dist/index.js +17 -69
- 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 +50 -30
- package/dist/llm/compat.js +149 -133
- 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 +29 -8
- package/dist/llm/modelCatalog.js +28 -66
- package/dist/llm/openAiCodex.d.ts +15 -7
- package/dist/llm/openAiCodex.js +57 -45
- package/dist/llm/openAiCodexWebSocket.d.ts +48 -15
- package/dist/llm/openAiCodexWebSocket.js +101 -147
- 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 +116 -34
- package/dist/llm/providers.js +298 -129
- package/dist/llm/retry.d.ts +11 -47
- package/dist/llm/retry.js +33 -52
- package/dist/llm/router.d.ts +54 -36
- package/dist/llm/router.js +91 -78
- package/dist/llm/sse.d.ts +44 -37
- package/dist/llm/sse.js +43 -35
- 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 +94 -6
- package/dist/plugin.adapter.js +18 -13
- 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 +2 -1
- 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 +60 -26
- package/dist/ports/model-endpoints.port.js +17 -3
- 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 +9 -2
- package/dist/ports/models-transport.port.js +4 -1
- 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 +17 -20
- package/dist/settings/memory-settings.adapter.d.ts +3 -1
- package/dist/settings/memory-settings.adapter.js +1 -1
- 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 +10 -2
- package/dist/settings/settings-value.entity.functions.d.ts +31 -9
- package/dist/settings/settings-value.entity.functions.js +5 -6
- 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 +17 -3
- 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 +18 -17
- package/dist/transport/models-transport.adapter.js +28 -31
- package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
- package/dist/transport/resilient-http-client.adapter.js +177 -0
- package/package.json +11 -5
- package/dist/auth/anthropicOAuth.test.d.ts +0 -1
- package/dist/auth/credential-refresh.entity.functions.test.d.ts +0 -1
- package/dist/auth/localAuth.test.d.ts +0 -1
- package/dist/auth/memory-auth.adapter.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/openAiCodexWebSocket.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/memory-settings.adapter.test.d.ts +0 -1
- package/dist/settings/roleView.test.d.ts +0 -1
- package/dist/settings/settings-value.entity.functions.test.d.ts +0 -1
- package/dist/transport/http-proxy.entity.functions.test.d.ts +0 -1
- package/dist/transport/models-transport.adapter.test.d.ts +0 -1
- package/dist/transport/recording-proxy.testing.d.ts +0 -33
package/dist/llm/compat.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { AiError, LanguageModel, Prompt, Tool } from "effect/ai";
|
|
2
|
-
import { Effect,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Cause, Effect, Match, MutableRef, Option, Ref, Schema, Stream } from "effect";
|
|
3
|
+
import { Base64 } from "effect/encoding";
|
|
4
|
+
import { FetchHttpClient, HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
|
|
5
|
+
import { isTurnError, ModelId, ModelSelection, ProviderId, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, ImageMediaType, mediaLabel, reasonOf } from "@xandreed/core";
|
|
6
|
+
import { strictJsonSchema, toolParametersSchema } from "@xandreed/core/internal";
|
|
7
|
+
import { finishReasonFromWire, sseStreamParts, toolCallParams, usageFromCompletion } from "./sse.js";
|
|
8
|
+
import { decodeChatCompletion, gatewayErrorOf, isModelRejection, isQuotaExhausted } from "./chat-wire.entity.functions.js";
|
|
9
|
+
import { publicReasoningEffort } from "./modelCatalog.js";
|
|
5
10
|
import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
})
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}), aiUnknown = (moduleName, method, e) => AiError.make({ module: moduleName, method, reason: new AiError.UnknownError({ description: redactCredentials(String(e)) }) }), invalidOutput = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidOutputError({ description }) }), requestInfo = (chatUrl) => ({
|
|
11
|
+
import { transportFetch } from "../transport/models-transport.adapter.js";
|
|
12
|
+
import { completeModelAttempt, modelAttemptOf, observeModelAttempt, retainModelAttempt } from "./model-completion.adapter.js";
|
|
13
|
+
import { ProviderUsageBody } from "./model-usage.entity.js";
|
|
14
|
+
import { resilientRequest, withModelRequestCall, withModelRequestStream } from "../transport/resilient-http-client.adapter.js";
|
|
15
|
+
import { classifyAiError } from "./provider-failure.entity.functions.js";
|
|
16
|
+
import { annotateCompatRequest, observeCompatUsage } from "./compat-telemetry.adapter.js";
|
|
17
|
+
const gatewayFailure = (moduleName, status, body, http) => {
|
|
18
|
+
const error = gatewayErrorOf(body), message = Option.getOrElse(Option.flatMap(error, (found) => Option.fromNullishOr(found.message)), () => "");
|
|
19
|
+
if (Option.exists(error, isModelRejection))
|
|
20
|
+
return new AiError.InvalidRequestError({ description: message.length > 0 ? message : "the selected model is not supported" });
|
|
21
|
+
if (isQuotaExhausted(Option.some(status), error))
|
|
22
|
+
return new AiError.QuotaExhaustedError({ metadata: { [moduleName]: { ...Option.getOrElse(error, () => ({})) } }, http });
|
|
23
|
+
return AiError.reasonFromHttpStatus({ status, http, description: body.slice(0, 500) });
|
|
24
|
+
}, aiUnknown = (moduleName, method, error) => {
|
|
25
|
+
const cause = HttpClientError.isHttpClientError(error) && error.cause !== void 0 ? error.cause : error;
|
|
26
|
+
return AiError.isAiError(cause) || isTurnError(cause) ? cause : AiError.make({ module: moduleName, method, reason: new AiError.UnknownError({ description: redactCredentials(reasonOf(Cause.die(cause))) }) });
|
|
27
|
+
}, invalidOutput = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidOutputError({ description }) }), invalidResponse = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidResponseError({ description }) }), requestInfo = (chatUrl) => ({
|
|
24
28
|
method: "POST",
|
|
25
29
|
url: chatUrl,
|
|
26
30
|
urlParams: [],
|
|
@@ -33,9 +37,9 @@ const record = (value) => typeof value === "object" && value !== null ? value :
|
|
|
33
37
|
}, filePartLabel = (part) => {
|
|
34
38
|
const data = part.data;
|
|
35
39
|
return isImageType(part.mediaType) && !(data instanceof URL) ? mediaLabel({ mediaType: part.mediaType, bytes: decodedBytes(data), ...part.fileName === void 0 ? {} : { name: part.fileName } }) : `[file omitted: ${[part.mediaType, ...part.fileName === void 0 ? [] : [part.fileName]].join(", ")}]`;
|
|
36
|
-
},
|
|
40
|
+
}, isSendableImage = (part) => isImageType(part.mediaType), imageUrlPart = (part) => {
|
|
37
41
|
const data = part.data;
|
|
38
|
-
return { type: "image_url", image_url: { url: data instanceof URL ? data.toString() : data instanceof Uint8Array ? `data:${part.mediaType};base64,${
|
|
42
|
+
return { type: "image_url", image_url: { url: data instanceof URL ? data.toString() : data instanceof Uint8Array ? `data:${part.mediaType};base64,${Base64.encode(data)}` : /^(data:|https?:\/\/)/i.test(data) ? data : `data:${part.mediaType};base64,${data}` } };
|
|
39
43
|
}, nonEmpty = (line) => line.length > 0, toolResultText = (part, images) => part.modelOutput === void 0 ? typeof part.result === "string" ? part.result : JSON.stringify(part.result ?? null) : part.modelOutput.flatMap((output) => output.type === "text" ? [output.text] : images && isSendableImage(output) ? [] : [filePartLabel(output)]).filter(nonEmpty).join(`
|
|
40
44
|
`), userText = (message) => {
|
|
41
45
|
const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), labels = message.content.flatMap((p) => p.type === "file" ? [filePartLabel(p)] : []);
|
|
@@ -50,16 +54,16 @@ export const toChatMessages = (prompt, options = {}) => {
|
|
|
50
54
|
role: "user",
|
|
51
55
|
content: message.content.flatMap((p) => p.type === "text" ? p.text.length === 0 ? [] : [{ type: "text", text: p.text }] : isSendableImage(p) ? [imageUrlPart(p)] : [{ type: "text", text: filePartLabel(p) }])
|
|
52
56
|
};
|
|
53
|
-
}, folded = prompt.content.reduce((fold, message) => {
|
|
54
|
-
|
|
55
|
-
const user = userMessage(
|
|
57
|
+
}, folded = prompt.content.reduce((fold, message) => Match.value(message).pipe(Match.discriminatorsExhaustive("role")({
|
|
58
|
+
user: (asked) => {
|
|
59
|
+
const user = userMessage(asked);
|
|
56
60
|
if (fold.pending.length === 0)
|
|
57
61
|
return { out: [...fold.out, user], pending: [] };
|
|
58
62
|
const parts = contentParts(user.content), own = parts.some((part) => typeof part === "object" && part !== null && ("type" in part) && part.type === "image_url");
|
|
59
63
|
return { out: [...fold.out, { role: "user", content: [...fold.pending, ...own ? [{ type: "text", text: USER_CAPTION }] : [], ...parts] }], pending: [] };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const results =
|
|
64
|
+
},
|
|
65
|
+
tool: (tool) => {
|
|
66
|
+
const results = tool.content.flatMap((p) => p.type === "tool-result" && !p.providerExecuted ? [p] : []), sent = results.flatMap((p) => {
|
|
63
67
|
const parts = images ? (p.modelOutput ?? []).flatMap((output) => output.type === "file" && isSendableImage(output) ? [output] : []) : [];
|
|
64
68
|
return parts.length === 0 ? [] : [{ type: "text", text: `Images returned by ${p.name} (call ${p.id}):` }, ...parts.map(imageUrlPart)];
|
|
65
69
|
}), toolMessages = results.map((p) => {
|
|
@@ -67,44 +71,43 @@ export const toChatMessages = (prompt, options = {}) => {
|
|
|
67
71
|
return { role: "tool", tool_call_id: p.id, content: text.length === 0 && hasImages ? "[the images this call returned follow]" : text };
|
|
68
72
|
});
|
|
69
73
|
return { out: [...fold.out, ...toolMessages], pending: [...fold.pending, ...sent] };
|
|
74
|
+
},
|
|
75
|
+
system: (system) => ({ out: [...flush(fold), { role: "system", content: system.content }], pending: [] }),
|
|
76
|
+
assistant: (assistant) => {
|
|
77
|
+
const text = assistant.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), toolCalls = assistant.content.flatMap((p) => p.type === "tool-call" && !p.providerExecuted ? [
|
|
78
|
+
{
|
|
79
|
+
id: p.id,
|
|
80
|
+
type: "function",
|
|
81
|
+
function: { name: p.name, arguments: JSON.stringify(p.params ?? {}) }
|
|
82
|
+
}
|
|
83
|
+
] : []);
|
|
84
|
+
return {
|
|
85
|
+
out: [...flush(fold), {
|
|
86
|
+
role: "assistant",
|
|
87
|
+
content: text.length > 0 ? text : null,
|
|
88
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
89
|
+
}],
|
|
90
|
+
pending: []
|
|
91
|
+
};
|
|
70
92
|
}
|
|
71
|
-
|
|
72
|
-
if (message.role === "system")
|
|
73
|
-
return { out: [...out, { role: "system", content: message.content }], pending: [] };
|
|
74
|
-
const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), toolCalls = message.content.flatMap((p) => p.type === "tool-call" ? [
|
|
75
|
-
{
|
|
76
|
-
id: p.id,
|
|
77
|
-
type: "function",
|
|
78
|
-
function: { name: p.name, arguments: JSON.stringify(p.params ?? {}) }
|
|
79
|
-
}
|
|
80
|
-
] : []);
|
|
81
|
-
return {
|
|
82
|
-
out: [...out, {
|
|
83
|
-
role: "assistant",
|
|
84
|
-
content: text.length > 0 ? text : null,
|
|
85
|
-
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
86
|
-
}],
|
|
87
|
-
pending: []
|
|
88
|
-
};
|
|
89
|
-
}, { out: [], pending: [] });
|
|
93
|
+
})), { out: [], pending: [] });
|
|
90
94
|
return flush(folded);
|
|
91
|
-
}, promptWithImageLabels = (prompt) => Prompt.fromMessages(prompt.content.map((message) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})), toChatTools = (tools) => tools.flatMap((tool) => Tool.isUserDefined(tool) ? [
|
|
95
|
+
}, promptWithImageLabels = (prompt) => Prompt.fromMessages(prompt.content.map((message) => Match.value(message).pipe(Match.discriminatorsExhaustive("role")({
|
|
96
|
+
user: (user) => user.content.some((p) => p.type === "file") ? Prompt.makeMessage("user", { content: [Prompt.makePart("text", { text: userText(user) })], options: user.options }) : user,
|
|
97
|
+
tool: (tool) => tool.content.some((p) => p.type === "tool-result" && p.modelOutput !== void 0) ? Prompt.makeMessage("tool", {
|
|
98
|
+
content: tool.content.map((p) => p.type !== "tool-result" || p.modelOutput === void 0 ? p : Prompt.makePart("tool-result", {
|
|
99
|
+
id: p.id,
|
|
100
|
+
name: p.name,
|
|
101
|
+
isFailure: p.isFailure,
|
|
102
|
+
result: toolResultText(p, !1),
|
|
103
|
+
providerExecuted: p.providerExecuted,
|
|
104
|
+
options: p.options
|
|
105
|
+
})),
|
|
106
|
+
options: tool.options
|
|
107
|
+
}) : tool,
|
|
108
|
+
system: (system) => system,
|
|
109
|
+
assistant: (assistant) => assistant
|
|
110
|
+
})))), toChatTools = (tools) => tools.flatMap((tool) => Tool.isUserDefined(tool) ? [
|
|
108
111
|
{
|
|
109
112
|
type: "function",
|
|
110
113
|
function: {
|
|
@@ -117,10 +120,10 @@ export const toChatMessages = (prompt, options = {}) => {
|
|
|
117
120
|
const toToolChoice = (choice) => {
|
|
118
121
|
if (choice === "none" || choice === "required")
|
|
119
122
|
return choice;
|
|
120
|
-
if (typeof choice === "object" &&
|
|
123
|
+
if (typeof choice === "object" && "tool" in choice)
|
|
121
124
|
return {
|
|
122
125
|
type: "function",
|
|
123
|
-
function: { name: choice.tool }
|
|
126
|
+
function: { name: String(choice.tool) }
|
|
124
127
|
};
|
|
125
128
|
return "auto";
|
|
126
129
|
};
|
|
@@ -132,26 +135,23 @@ export const thinkingParams = (model) => {
|
|
|
132
135
|
if (/qwen/i.test(model))
|
|
133
136
|
return { enable_thinking: !0 };
|
|
134
137
|
return {};
|
|
135
|
-
}, fromChatCompletion = (moduleName, body, reading = {})
|
|
138
|
+
}, fromChatCompletion = Effect.fnUntraced(function* (moduleName, body, reading = {}) {
|
|
136
139
|
const choice = body.choices?.[0];
|
|
137
140
|
if (choice === void 0)
|
|
138
|
-
return yield* Effect.fail(
|
|
139
|
-
const message = choice.message ?? {}, reasoning = message.reasoning_content ?? message.reasoning, text = message.content, toolCalls = yield* Effect.forEach(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}),
|
|
146
|
-
catch: () => invalidOutput(moduleName, "generateText", `tool call ${tc.function?.name ?? "?"} carried unparseable JSON arguments`)
|
|
147
|
-
}));
|
|
141
|
+
return yield* Effect.fail(invalidResponse(moduleName, "generateText", "the completion carried no choices"));
|
|
142
|
+
const message = choice.message ?? {}, reasoning = message.reasoning_content ?? message.reasoning, text = message.content, calls = message.tool_calls ?? [], reason = finishReasonFromWire(Option.fromNullishOr(choice.finish_reason), calls.length > 0), toolCalls = yield* Effect.forEach(calls, (tc) => toolCallParams(tc.function?.arguments ?? "{}", reason, () => invalidOutput(moduleName, "generateText", `tool call ${tc.function?.name ?? "?"} carried unparseable JSON arguments`)).pipe(Effect.map((params) => ({
|
|
143
|
+
type: "tool-call",
|
|
144
|
+
id: tc.id ?? "",
|
|
145
|
+
name: tc.function?.name ?? "",
|
|
146
|
+
params
|
|
147
|
+
}))), { concurrency: 1 });
|
|
148
148
|
return [
|
|
149
149
|
...reasoning !== null && reasoning !== void 0 && reasoning.length > 0 ? [{ type: "reasoning", text: reasoning }] : [],
|
|
150
150
|
...text !== null && text !== void 0 && text.length > 0 ? [{ type: "text", text }] : [],
|
|
151
151
|
...toolCalls,
|
|
152
152
|
{
|
|
153
153
|
type: "finish",
|
|
154
|
-
reason
|
|
154
|
+
reason,
|
|
155
155
|
usage: usageFromCompletion(body.usage, reading)
|
|
156
156
|
}
|
|
157
157
|
];
|
|
@@ -166,11 +166,11 @@ const requestSettings = (config, cacheKey, policy) => ({
|
|
|
166
166
|
onNone: () => ({}),
|
|
167
167
|
onSome: (value) => ({
|
|
168
168
|
...value.maxOutputTokens === void 0 ? {} : { max_tokens: value.maxOutputTokens },
|
|
169
|
-
...config.thinking === "disabled" || config.reasoningEffort !== void 0 ? {} : { reasoning_effort: value.effort }
|
|
169
|
+
...config.thinking === "disabled" || config.reasoningEffort !== void 0 ? {} : { reasoning_effort: publicReasoningEffort(value.effort) }
|
|
170
170
|
})
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
|
-
}), chatRequestBody = (config, options, streaming) => Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }).pipe(Effect.map(({ cacheKey, policy }) => {
|
|
173
|
+
}), chatRequestBody = (config, options, streaming) => Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }, { concurrency: 1 }).pipe(Effect.map(({ cacheKey, policy }) => {
|
|
174
174
|
const tools = toChatTools(options.tools), settings = requestSettings(config, cacheKey, policy);
|
|
175
175
|
return {
|
|
176
176
|
model: config.model,
|
|
@@ -185,68 +185,84 @@ const requestSettings = (config, cacheKey, policy) => ({
|
|
|
185
185
|
...settings.extensions,
|
|
186
186
|
...tools.length > 0 ? { tools, tool_choice: toToolChoice(options.toolChoice) } : {}
|
|
187
187
|
};
|
|
188
|
-
})), postChat = (config, method, body, buffered = !1) => {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
})), postChat = (config, method, body, buffered = !1, observeUsage) => {
|
|
189
|
+
const selected = {}, guarded = Effect.gen(function* () {
|
|
190
|
+
const client = yield* Effect.service(HttpClient.HttpClient).pipe(Effect.provide(FetchHttpClient.layer)), metadata = yield* Ref.make({ statusText: "" }), fetch = config.fetchImpl ?? (yield* FetchHttpClient.Fetch), trackedFetch = transportFetch({ fetch: (input, init) => fetch(input, init).then((response) => {
|
|
191
|
+
if (init?.signal?.aborted !== !0)
|
|
192
|
+
MutableRef.set(metadata.ref, { statusText: response.statusText });
|
|
193
|
+
return response;
|
|
194
|
+
}) }), response = yield* resilientRequest(client, HttpClientRequest.post(config.chatUrl).pipe(HttpClientRequest.setHeaders({
|
|
195
|
+
"content-type": "application/json",
|
|
196
|
+
...config.apiKey === void 0 || config.apiKey.length === 0 ? {} : { authorization: `Bearer ${config.apiKey}` }
|
|
197
|
+
}), HttpClientRequest.bodyJsonUnsafe(body)), { protocol: "compat", settled: buffered, ...config.beforeRequest === void 0 ? {} : { beforeSend: config.beforeRequest(body) } }).pipe(Effect.provideService(FetchHttpClient.Fetch, trackedFetch)), content = buffered ? yield* response.arrayBuffer : yield* Stream.toReadableStreamEffect(response.stream);
|
|
198
|
+
if (buffered && content instanceof ArrayBuffer) {
|
|
199
|
+
const body = Schema.decodeUnknownOption(Schema.fromJsonString(ProviderUsageBody))(new TextDecoder().decode(content));
|
|
200
|
+
yield* Option.match(body, { onNone: () => Effect.void, onSome: (value) => observeModelAttempt(response.modelAttempt.completion, value.usage, !0, config.usage?.reasoning ?? !0).pipe(Effect.andThen(observeUsage?.(value.usage, !0) ?? Effect.void)) });
|
|
201
|
+
yield* completeModelAttempt(response.modelAttempt.completion, { _tag: "Completed" });
|
|
202
|
+
}
|
|
203
|
+
const { statusText } = yield* Ref.get(metadata);
|
|
204
|
+
return yield* Effect.try({
|
|
205
|
+
try: () => retainModelAttempt(response, Object.defineProperty(new Response([204, 205, 304].includes(response.status) ? null : content, {
|
|
206
|
+
status: response.status,
|
|
207
|
+
statusText,
|
|
208
|
+
headers: response.headers
|
|
209
|
+
}), "url", { value: response.url })),
|
|
210
|
+
catch: (error) => aiUnknown(config.moduleName, method, error)
|
|
211
|
+
});
|
|
212
|
+
}).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(aiUnknown(config.moduleName, method, error)))).pipe(Effect.tap((response) => Effect.sync(() => {
|
|
213
|
+
selected.response = response;
|
|
214
|
+
}))).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(error)));
|
|
215
|
+
return config.aroundRequest === void 0 ? guarded : config.aroundRequest(body, guarded).pipe(Effect.map((returned) => selected.response === void 0 ? returned : retainModelAttempt(selected.response, returned)));
|
|
205
216
|
}, failStatus = (config, method, res) => Effect.gen(function* () {
|
|
206
217
|
const text = yield* Effect.tryPromise({
|
|
207
218
|
try: () => res.text(),
|
|
208
219
|
catch: (e) => aiUnknown(config.moduleName, method, e)
|
|
209
|
-
}),
|
|
210
|
-
if (Option.isSome(semantic))
|
|
211
|
-
return yield* Effect.fail(semantic.value);
|
|
212
|
-
const headers = {};
|
|
220
|
+
}), headers = {};
|
|
213
221
|
res.headers.forEach((value, headerName) => {
|
|
214
222
|
headers[headerName] = value;
|
|
215
223
|
});
|
|
216
224
|
return yield* Effect.fail(AiError.make({
|
|
217
225
|
module: config.moduleName,
|
|
218
226
|
method,
|
|
219
|
-
reason:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
reason: gatewayFailure(config.moduleName, res.status, text, {
|
|
228
|
+
request: requestInfo(config.chatUrl),
|
|
229
|
+
response: { status: res.status, headers },
|
|
230
|
+
body: text.slice(0, 500)
|
|
223
231
|
})
|
|
224
232
|
}));
|
|
225
233
|
});
|
|
226
|
-
export const makeCompatLanguageModel = (config) =>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
234
|
+
export const makeCompatLanguageModel = (config) => {
|
|
235
|
+
const selection = new ModelSelection({ provider: ProviderId.make(config.moduleName), modelId: ModelId.make(config.model) });
|
|
236
|
+
return LanguageModel.make({
|
|
237
|
+
generateText: (options) => withModelRequestCall(options.responseFormat.type === "json" ? "Object" : "Text", selection, Effect.gen(function* () {
|
|
238
|
+
yield* annotateCompatRequest(options.span, config.moduleName, config.model);
|
|
239
|
+
const res = yield* postChat(config, "generateText", yield* chatRequestBody(config, options, !1), !0, (usage, final) => observeCompatUsage(options.span, usage, final, config.usage?.reasoning ?? !0));
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
return yield* failStatus(config, "generateText", res);
|
|
242
|
+
const text = yield* Effect.tryPromise({
|
|
243
|
+
try: () => res.text(),
|
|
244
|
+
catch: (e) => aiUnknown(config.moduleName, "generateText", e)
|
|
245
|
+
}), parsed = yield* decodeChatCompletion(config.moduleName, text);
|
|
246
|
+
return yield* fromChatCompletion(config.moduleName, parsed, config.usage);
|
|
247
|
+
})).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(classifyAiError(error, "compat")))),
|
|
248
|
+
streamText: (options) => withModelRequestStream("Stream", selection, Stream.unwrap(Effect.gen(function* () {
|
|
249
|
+
yield* annotateCompatRequest(options.span, config.moduleName, config.model);
|
|
250
|
+
const res = yield* postChat(config, "streamText", yield* chatRequestBody(config, options, !0));
|
|
251
|
+
if (!res.ok)
|
|
252
|
+
return yield* failStatus(config, "streamText", res);
|
|
253
|
+
const body = res.body;
|
|
254
|
+
if (body === null)
|
|
255
|
+
return yield* Effect.fail(invalidResponse(config.moduleName, "streamText", "the streaming response carried no body"));
|
|
256
|
+
return sseStreamParts({
|
|
257
|
+
moduleName: config.moduleName,
|
|
258
|
+
body,
|
|
259
|
+
attempt: modelAttemptOf(res),
|
|
260
|
+
...config.usage === void 0 ? {} : { usage: config.usage },
|
|
261
|
+
observeUsage: (usage, final) => observeCompatUsage(options.span, usage, final, config.usage?.reasoning ?? !0)
|
|
262
|
+
});
|
|
263
|
+
}))).pipe(Stream.catch((error) => isTurnError(error) ? Stream.die(error) : Stream.fail(classifyAiError(error, "compat"))))
|
|
264
|
+
}).pipe(Effect.map((model) => describeModel(model, Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }, { concurrency: 1 }).pipe(Effect.map(({ cacheKey, policy }) => {
|
|
265
|
+
const settings = requestSettings(config, cacheKey, policy);
|
|
266
|
+
return { provider: config.moduleName, model: config.model, settings: { ...settings.sampling, ...settings.extensions } };
|
|
267
|
+
})))));
|
|
268
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Deferred, Effect, Exit, Option, Ref } from "effect";
|
|
2
|
+
import { ModelCallId } from "@xandreed/core";
|
|
3
|
+
import type { ModelCallMethod, ModelCallOutcome, ModelCallPurpose, ModelCallRecorder, ModelSelection, RunId, RunOrdinal, TurnError } from "@xandreed/core";
|
|
4
|
+
import type { ProviderFailureProtocol } from "./provider-failure.entity.js";
|
|
5
|
+
import { ModelCompletionState } from "./model-completion.entity.js";
|
|
6
|
+
import type { ProviderUsageObservation } from "./model-usage.entity.js";
|
|
7
|
+
export interface ModelCompletionCall {
|
|
8
|
+
readonly callId: ModelCallId;
|
|
9
|
+
readonly attempts: Ref.Ref<ReadonlyArray<ModelAttempt>>;
|
|
10
|
+
}
|
|
11
|
+
export interface ModelAttempt {
|
|
12
|
+
readonly callId: ModelCallId;
|
|
13
|
+
readonly ordinal: number;
|
|
14
|
+
readonly startedAt: number;
|
|
15
|
+
readonly protocol: ProviderFailureProtocol;
|
|
16
|
+
readonly purpose: ModelCallPurpose;
|
|
17
|
+
readonly method: ModelCallMethod;
|
|
18
|
+
readonly model: Option.Option<ModelSelection>;
|
|
19
|
+
readonly runId: Option.Option<RunId>;
|
|
20
|
+
readonly run: Option.Option<RunOrdinal>;
|
|
21
|
+
readonly step: Option.Option<number>;
|
|
22
|
+
readonly recorder: ModelCallRecorder;
|
|
23
|
+
readonly usage: Ref.Ref<ProviderUsageObservation>;
|
|
24
|
+
readonly phase: Ref.Ref<ModelCompletionState>;
|
|
25
|
+
readonly ack: Deferred.Deferred<void, TurnError>;
|
|
26
|
+
}
|
|
27
|
+
export declare const makeCompletionCall: () => Effect.Effect<ModelCompletionCall, never, never>;
|
|
28
|
+
export declare const beginModelAttempt: (call: ModelCompletionCall, ordinal: number, protocol: "anthropic" | "openai" | "compat") => Effect.Effect<ModelAttempt, never, never>;
|
|
29
|
+
export declare const modelAttemptOf: (response: object) => Option.Option<ModelAttempt>;
|
|
30
|
+
export declare const retainModelAttempt: <A extends object>(source: object, target: A) => A;
|
|
31
|
+
export declare const attachModelAttempt: <A extends object>(attempt: ModelAttempt, response: A) => A;
|
|
32
|
+
export declare const observeModelAttempt: (attempt: ModelAttempt, value: unknown, final: boolean, reasoning?: boolean) => Effect.Effect<void>;
|
|
33
|
+
export declare const completeModelAttempt: (attempt: ModelAttempt, outcome: {
|
|
34
|
+
readonly _tag: "Completed";
|
|
35
|
+
} | {
|
|
36
|
+
readonly _tag: "Failed";
|
|
37
|
+
readonly failure: Option.Option<"Unknown" | "ContextOverflow" | "Authentication" | "InvalidRequest" | "RateLimit" | "Quota" | "Transient" | "InvalidResponse" | "Timeout">;
|
|
38
|
+
} | {
|
|
39
|
+
readonly _tag: "Interrupted";
|
|
40
|
+
}) => Effect.Effect<void, TurnError, never>;
|
|
41
|
+
export declare const modelOutcomeOfExit: <A, E>(exit: Exit.Exit<A, E>) => ModelCallOutcome;
|
|
42
|
+
/** Provider/SDK cleanup settles only attempts that no raw boundary has already claimed. */
|
|
43
|
+
export declare const completePendingAttempts: <A, E>(call: ModelCompletionCall, exit: Exit.Exit<A, E>) => Effect.Effect<void, TurnError, never>;
|
|
44
|
+
export declare const observeResponseUsage: (response: object, value: unknown, final: boolean, reasoning?: boolean) => Effect.Effect<void>;
|
|
45
|
+
export declare const completeResponseAttempt: (response: object, outcome: ModelCallOutcome) => Effect.Effect<void, TurnError>;
|
|
46
|
+
/** The fixed native SDK client surface has AiError only; retain the original recorder failure as a defect. */
|
|
47
|
+
export declare const completeSdkResponseAttempt: (response: object, outcome: ModelCallOutcome) => Effect.Effect<void>;
|
|
48
|
+
/** Rejected/compat JSON can report usage even when no SDK completion is accepted. */
|
|
49
|
+
export declare const observeModelBody: (attempt: ModelAttempt, body: string, final: boolean, reasoning?: boolean) => Effect.Effect<void>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Cause, Clock, Deferred, Effect, Exit, Option, Ref, Schema } from "effect";
|
|
2
|
+
import { CurrentAgentRun, CurrentAgentStep, CurrentModelCallRecorder, ModelCallId, modelFailureOf, recordCompletion } from "@xandreed/core";
|
|
3
|
+
import { randomUuid } from "@xandreed/core/internal";
|
|
4
|
+
import { CurrentModelCallMethod, CurrentModelCallPurpose, CurrentModelCallSelection } from "../ports/model-call-completion.port.js";
|
|
5
|
+
import { ModelCompletionState } from "./model-completion.entity.js";
|
|
6
|
+
import { claimModelCompletion } from "./model-completion.entity.functions.js";
|
|
7
|
+
import { noProviderUsage, ProviderUsageBody } from "./model-usage.entity.js";
|
|
8
|
+
import { modelCallUsageOf, observeProviderUsage, providerUsageCounts } from "./model-usage.entity.functions.js";
|
|
9
|
+
const attempts = new WeakMap;
|
|
10
|
+
export const makeCompletionCall = Effect.fnUntraced(function* () {
|
|
11
|
+
return { callId: ModelCallId.make(yield* randomUuid), attempts: yield* Ref.make([]) };
|
|
12
|
+
}), beginModelAttempt = Effect.fnUntraced(function* (call, ordinal, protocol) {
|
|
13
|
+
const run = yield* CurrentAgentRun, attempt = {
|
|
14
|
+
callId: call.callId,
|
|
15
|
+
ordinal,
|
|
16
|
+
protocol,
|
|
17
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
18
|
+
purpose: yield* CurrentModelCallPurpose,
|
|
19
|
+
method: yield* CurrentModelCallMethod,
|
|
20
|
+
model: yield* CurrentModelCallSelection,
|
|
21
|
+
runId: Option.map(run, (value) => value.id),
|
|
22
|
+
run: Option.map(run, (value) => value.ordinal),
|
|
23
|
+
step: Option.isSome(run) ? yield* CurrentAgentStep : Option.none(),
|
|
24
|
+
recorder: yield* CurrentModelCallRecorder,
|
|
25
|
+
usage: yield* Ref.make(noProviderUsage),
|
|
26
|
+
phase: yield* Ref.make({ _tag: "Open" }),
|
|
27
|
+
ack: yield* Deferred.make()
|
|
28
|
+
};
|
|
29
|
+
yield* Ref.update(call.attempts, (before) => [...before, attempt]);
|
|
30
|
+
return attempt;
|
|
31
|
+
}), modelAttemptOf = (response) => Option.fromNullishOr(attempts.get(response)), retainModelAttempt = (source, target) => {
|
|
32
|
+
Option.match(modelAttemptOf(source), { onNone: () => {
|
|
33
|
+
return;
|
|
34
|
+
}, onSome: (attempt) => attempts.set(target, attempt) });
|
|
35
|
+
return target;
|
|
36
|
+
}, attachModelAttempt = (attempt, response) => {
|
|
37
|
+
attempts.set(response, attempt);
|
|
38
|
+
return response;
|
|
39
|
+
}, observeModelAttempt = (attempt, value, final, reasoning = !0) => Option.match(providerUsageCounts(attempt.protocol, value, reasoning), {
|
|
40
|
+
onNone: () => Effect.void,
|
|
41
|
+
onSome: (counts) => Ref.update(attempt.usage, (before) => observeProviderUsage(before, counts, final))
|
|
42
|
+
}), completeModelAttempt = Effect.fn("models.record_attempt")(function* (attempt, outcome) {
|
|
43
|
+
return yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
44
|
+
if (!(yield* Ref.modify(attempt.phase, claimModelCompletion)))
|
|
45
|
+
return yield* Deferred.await(attempt.ack);
|
|
46
|
+
const data = {
|
|
47
|
+
callId: attempt.callId,
|
|
48
|
+
attempt: attempt.ordinal,
|
|
49
|
+
purpose: attempt.purpose,
|
|
50
|
+
method: attempt.method,
|
|
51
|
+
model: attempt.model,
|
|
52
|
+
runId: attempt.runId,
|
|
53
|
+
run: attempt.run,
|
|
54
|
+
step: attempt.step,
|
|
55
|
+
durationMs: Math.max(0, (yield* Clock.currentTimeMillis) - attempt.startedAt),
|
|
56
|
+
usage: modelCallUsageOf(attempt.protocol, yield* Ref.get(attempt.usage)),
|
|
57
|
+
outcome
|
|
58
|
+
}, result = yield* Effect.exit(recordCompletion(data).pipe(Effect.provideService(CurrentModelCallRecorder, attempt.recorder))), phase = Exit.isSuccess(result) ? { _tag: "Recorded" } : { _tag: "RecordFailed" };
|
|
59
|
+
yield* Ref.set(attempt.phase, phase);
|
|
60
|
+
yield* Deferred.done(attempt.ack, result);
|
|
61
|
+
return yield* Exit.match(result, { onSuccess: () => Effect.void, onFailure: Effect.failCause });
|
|
62
|
+
}));
|
|
63
|
+
}), modelOutcomeOfExit = (exit) => Exit.match(exit, {
|
|
64
|
+
onSuccess: () => ({ _tag: "Completed" }),
|
|
65
|
+
onFailure: (cause) => Cause.hasInterrupts(cause) ? { _tag: "Interrupted" } : { _tag: "Failed", failure: modelFailureOf(Cause.squash(cause)) }
|
|
66
|
+
}), completePendingAttempts = Effect.fnUntraced(function* (call, exit) {
|
|
67
|
+
yield* Effect.forEach(yield* Ref.get(call.attempts), (attempt) => Effect.flatMap(Ref.get(attempt.phase), (state) => ModelCompletionState.match(state, { Open: () => completeModelAttempt(attempt, modelOutcomeOfExit(exit)), Recorded: () => Effect.void, Recording: () => Deferred.await(attempt.ack), RecordFailed: () => Deferred.await(attempt.ack) })), { concurrency: 1, discard: !0 });
|
|
68
|
+
}), observeResponseUsage = (response, value, final, reasoning = !0) => Option.match(modelAttemptOf(response), {
|
|
69
|
+
onNone: () => Effect.void,
|
|
70
|
+
onSome: (attempt) => observeModelAttempt(attempt, value, final, reasoning)
|
|
71
|
+
}), completeResponseAttempt = (response, outcome) => Option.match(modelAttemptOf(response), {
|
|
72
|
+
onNone: () => Effect.void,
|
|
73
|
+
onSome: (attempt) => completeModelAttempt(attempt, outcome)
|
|
74
|
+
}), completeSdkResponseAttempt = (response, outcome) => completeResponseAttempt(response, outcome).pipe(Effect.catch((error) => Effect.die(error))), observeModelBody = (attempt, body, final, reasoning = !0) => Option.match(Schema.decodeUnknownOption(Schema.fromJsonString(ProviderUsageBody))(body), {
|
|
75
|
+
onNone: () => Effect.void,
|
|
76
|
+
onSome: (value) => observeModelAttempt(attempt, value.usage, final, reasoning)
|
|
77
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** The ACK result lives in one Deferred; this phase controls claiming and outer cleanup. */
|
|
3
|
+
export declare const ModelCompletionState: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Open", {}>, Schema.TaggedStruct<"Recording", {}>, Schema.TaggedStruct<"Recorded", {}>, Schema.TaggedStruct<"RecordFailed", {}>]>;
|
|
4
|
+
export type ModelCompletionState = typeof ModelCompletionState.Type;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ModelCompletionState } from "./model-completion.entity.js";
|
|
2
|
+
export const claimModelCompletion = (state) => ModelCompletionState.match(state, {
|
|
3
|
+
Open: () => [!0, { _tag: "Recording" }],
|
|
4
|
+
Recording: () => [!1, state],
|
|
5
|
+
Recorded: () => [!1, state],
|
|
6
|
+
RecordFailed: () => [!1, state]
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const ModelCompletionState = Schema.Union([
|
|
3
|
+
Schema.TaggedStruct("Open", {}),
|
|
4
|
+
Schema.TaggedStruct("Recording", {}),
|
|
5
|
+
Schema.TaggedStruct("Recorded", {}),
|
|
6
|
+
Schema.TaggedStruct("RecordFailed", {})
|
|
7
|
+
]).pipe(Schema.toTaggedUnion("_tag"));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** One logical model request's bounded HTTP retries and pending-read timeouts. @public */
|
|
3
|
+
export declare const ModelRetryPolicy: Schema.Struct<{
|
|
4
|
+
readonly maxAttempts: Schema.Int;
|
|
5
|
+
readonly requestTimeoutMs: Schema.Int;
|
|
6
|
+
readonly bodyIdleTimeoutMs: Schema.Int;
|
|
7
|
+
readonly totalTimeoutMs: Schema.Int;
|
|
8
|
+
readonly retryAfterMaxMs: Schema.Int;
|
|
9
|
+
readonly backoff: Schema.Struct<{
|
|
10
|
+
readonly initialMs: Schema.Int;
|
|
11
|
+
readonly maxMs: Schema.Int;
|
|
12
|
+
readonly jitter: Schema.Number;
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
export type ModelRetryPolicy = typeof ModelRetryPolicy.Type;
|
|
16
|
+
/** Full defaults; an initial send counts toward maxAttempts. @public */
|
|
17
|
+
export declare const modelRetryDefaults: ModelRetryPolicy;
|
|
18
|
+
export declare const ModelRequestRung: Schema.Literals<readonly ["Primary", "Fallback"]>;
|
|
19
|
+
export type ModelRequestRung = typeof ModelRequestRung.Type;
|
|
20
|
+
/** Private attempt state, shared by one routed call and its distinct fallback. */
|
|
21
|
+
export declare const ModelRequestState: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Ready", {
|
|
22
|
+
readonly rung: Schema.Literals<readonly ["Primary", "Fallback"]>;
|
|
23
|
+
readonly attempts: Schema.Int;
|
|
24
|
+
}>, Schema.TaggedStruct<"Sending", {
|
|
25
|
+
readonly rung: Schema.Literals<readonly ["Primary", "Fallback"]>;
|
|
26
|
+
readonly attempts: Schema.Int;
|
|
27
|
+
}>, Schema.TaggedStruct<"Rejected", {
|
|
28
|
+
readonly rung: Schema.Literals<readonly ["Primary", "Fallback"]>;
|
|
29
|
+
readonly attempts: Schema.Int;
|
|
30
|
+
readonly kind: Schema.Literals<readonly ["ContextOverflow", "Authentication", "InvalidRequest", "RateLimit", "Quota", "Transient", "InvalidResponse", "Timeout", "Unknown"]>;
|
|
31
|
+
readonly replayable: Schema.Boolean;
|
|
32
|
+
readonly retryAt: Schema.optionalKey<Schema.Int>;
|
|
33
|
+
}>, Schema.TaggedStruct<"Committed", {
|
|
34
|
+
readonly rung: Schema.Literals<readonly ["Primary", "Fallback"]>;
|
|
35
|
+
readonly attempts: Schema.Int;
|
|
36
|
+
}>]>;
|
|
37
|
+
export type ModelRequestState = typeof ModelRequestState.Type;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { ModelFailureKind } from "@xandreed/core";
|
|
3
|
+
import { ModelRequestState } from "./model-retry-policy.entity.js";
|
|
4
|
+
import type { ModelRetryPolicy } from "./model-retry-policy.entity.js";
|
|
5
|
+
/** Parse a `retry-after` value: non-negative seconds (decimals as the provider SDKs read them) or an HTTP date, never arbitrary text. */
|
|
6
|
+
export declare const retryAfterMillis: (value: string, now: number) => Option.Option<number>;
|
|
7
|
+
/** Parse a `retry-after-ms` value: non-negative milliseconds. */
|
|
8
|
+
export declare const retryAfterMsMillis: (value: string) => Option.Option<number>;
|
|
9
|
+
/** A rejected response's server hint: `retry-after-ms` (milliseconds) first, then `retry-after`; none when neither parses. */
|
|
10
|
+
export declare const retryHintMillis: (headers: Readonly<Record<string, unknown>>, now: number) => Option.Option<number>;
|
|
11
|
+
/** One physical-attempt ordinal drives both bounded local delay and the capped server hint (milliseconds). */
|
|
12
|
+
export declare const retryDelayMillis: (policy: ModelRetryPolicy, attempt: number, unit: number, hint: Option.Option<number>) => number;
|
|
13
|
+
export declare const primaryAttemptLimit: (policy: ModelRetryPolicy, reserveFallback: boolean) => number;
|
|
14
|
+
export declare const retryableModelFailure: (kind: ModelFailureKind) => boolean;
|
|
15
|
+
export declare const startAttemptState: (state: ModelRequestState, policy: ModelRetryPolicy, reserveFallback: boolean) => Option.Option<ModelRequestState>;
|
|
16
|
+
export declare const enterFallbackState: (state: ModelRequestState, policy: ModelRetryPolicy, reserveFallback: boolean) => Option.Option<ModelRequestState>;
|