@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
package/dist/llm/compat.js
CHANGED
|
@@ -1,59 +1,113 @@
|
|
|
1
|
-
import { AiError, LanguageModel, Tool } from "effect/ai";
|
|
2
|
-
import { Effect,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { AiError, LanguageModel, Prompt, Tool } from "effect/ai";
|
|
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";
|
|
10
|
+
import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
|
|
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) => ({
|
|
23
28
|
method: "POST",
|
|
24
29
|
url: chatUrl,
|
|
25
30
|
urlParams: [],
|
|
26
31
|
headers: { "content-type": "application/json" }
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
}), isImageType = Schema.is(ImageMediaType), decodedBytes = (data) => {
|
|
33
|
+
if (typeof data !== "string")
|
|
34
|
+
return data.length;
|
|
35
|
+
const base64 = data.replace(/^data:[^,]*;base64,/i, "").replace(/\s+/g, ""), padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
36
|
+
return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
|
|
37
|
+
}, filePartLabel = (part) => {
|
|
38
|
+
const data = part.data;
|
|
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(", ")}]`;
|
|
40
|
+
}, isSendableImage = (part) => isImageType(part.mediaType), imageUrlPart = (part) => {
|
|
41
|
+
const data = part.data;
|
|
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}` } };
|
|
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(`
|
|
44
|
+
`), userText = (message) => {
|
|
45
|
+
const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), labels = message.content.flatMap((p) => p.type === "file" ? [filePartLabel(p)] : []);
|
|
46
|
+
return [text, ...labels].filter(nonEmpty).join(`
|
|
47
|
+
`);
|
|
48
|
+
}, contentParts = (content) => typeof content === "string" ? content.length === 0 ? [] : [{ type: "text", text: content }] : Array.isArray(content) ? content : [], USER_CAPTION = "The user's message:", flush = (fold) => fold.pending.length === 0 ? fold.out : [...fold.out, { role: "user", content: fold.pending }];
|
|
49
|
+
export const toChatMessages = (prompt, options = {}) => {
|
|
50
|
+
const images = options.images ?? !1, userMessage = (message) => {
|
|
51
|
+
if (!(images && message.content.some((p) => p.type === "file" && isSendableImage(p))))
|
|
52
|
+
return { role: "user", content: userText(message) };
|
|
53
|
+
return {
|
|
54
|
+
role: "user",
|
|
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) }])
|
|
56
|
+
};
|
|
57
|
+
}, folded = prompt.content.reduce((fold, message) => Match.value(message).pipe(Match.discriminatorsExhaustive("role")({
|
|
58
|
+
user: (asked) => {
|
|
59
|
+
const user = userMessage(asked);
|
|
60
|
+
if (fold.pending.length === 0)
|
|
61
|
+
return { out: [...fold.out, user], pending: [] };
|
|
62
|
+
const parts = contentParts(user.content), own = parts.some((part) => typeof part === "object" && part !== null && ("type" in part) && part.type === "image_url");
|
|
63
|
+
return { out: [...fold.out, { role: "user", content: [...fold.pending, ...own ? [{ type: "text", text: USER_CAPTION }] : [], ...parts] }], pending: [] };
|
|
64
|
+
},
|
|
65
|
+
tool: (tool) => {
|
|
66
|
+
const results = tool.content.flatMap((p) => p.type === "tool-result" && !p.providerExecuted ? [p] : []), sent = results.flatMap((p) => {
|
|
67
|
+
const parts = images ? (p.modelOutput ?? []).flatMap((output) => output.type === "file" && isSendableImage(output) ? [output] : []) : [];
|
|
68
|
+
return parts.length === 0 ? [] : [{ type: "text", text: `Images returned by ${p.name} (call ${p.id}):` }, ...parts.map(imageUrlPart)];
|
|
69
|
+
}), toolMessages = results.map((p) => {
|
|
70
|
+
const text = toolResultText(p, images), hasImages = images && (p.modelOutput ?? []).some((output) => output.type === "file" && isSendableImage(output));
|
|
71
|
+
return { role: "tool", tool_call_id: p.id, content: text.length === 0 && hasImages ? "[the images this call returned follow]" : text };
|
|
72
|
+
});
|
|
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
|
+
};
|
|
54
92
|
}
|
|
55
|
-
] : []);
|
|
56
|
-
|
|
93
|
+
})), { out: [], pending: [] });
|
|
94
|
+
return flush(folded);
|
|
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) ? [
|
|
57
111
|
{
|
|
58
112
|
type: "function",
|
|
59
113
|
function: {
|
|
@@ -66,10 +120,10 @@ export const toChatMessages = (prompt) => prompt.content.flatMap((message) => {
|
|
|
66
120
|
const toToolChoice = (choice) => {
|
|
67
121
|
if (choice === "none" || choice === "required")
|
|
68
122
|
return choice;
|
|
69
|
-
if (typeof choice === "object" &&
|
|
123
|
+
if (typeof choice === "object" && "tool" in choice)
|
|
70
124
|
return {
|
|
71
125
|
type: "function",
|
|
72
|
-
function: { name: choice.tool }
|
|
126
|
+
function: { name: String(choice.tool) }
|
|
73
127
|
};
|
|
74
128
|
return "auto";
|
|
75
129
|
};
|
|
@@ -81,27 +135,24 @@ export const thinkingParams = (model) => {
|
|
|
81
135
|
if (/qwen/i.test(model))
|
|
82
136
|
return { enable_thinking: !0 };
|
|
83
137
|
return {};
|
|
84
|
-
}, fromChatCompletion = (moduleName, body
|
|
138
|
+
}, fromChatCompletion = Effect.fnUntraced(function* (moduleName, body, reading = {}) {
|
|
85
139
|
const choice = body.choices?.[0];
|
|
86
140
|
if (choice === void 0)
|
|
87
|
-
return yield* Effect.fail(
|
|
88
|
-
const message = choice.message ?? {}, reasoning = message.reasoning_content ?? message.reasoning, text = message.content, toolCalls = yield* Effect.forEach(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}),
|
|
95
|
-
catch: () => invalidOutput(moduleName, "generateText", `tool call ${tc.function?.name ?? "?"} carried unparseable JSON arguments`)
|
|
96
|
-
}));
|
|
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 });
|
|
97
148
|
return [
|
|
98
149
|
...reasoning !== null && reasoning !== void 0 && reasoning.length > 0 ? [{ type: "reasoning", text: reasoning }] : [],
|
|
99
150
|
...text !== null && text !== void 0 && text.length > 0 ? [{ type: "text", text }] : [],
|
|
100
151
|
...toolCalls,
|
|
101
152
|
{
|
|
102
153
|
type: "finish",
|
|
103
|
-
reason
|
|
104
|
-
usage: usageFromCompletion(body.usage)
|
|
154
|
+
reason,
|
|
155
|
+
usage: usageFromCompletion(body.usage, reading)
|
|
105
156
|
}
|
|
106
157
|
];
|
|
107
158
|
});
|
|
@@ -115,16 +166,16 @@ const requestSettings = (config, cacheKey, policy) => ({
|
|
|
115
166
|
onNone: () => ({}),
|
|
116
167
|
onSome: (value) => ({
|
|
117
168
|
...value.maxOutputTokens === void 0 ? {} : { max_tokens: value.maxOutputTokens },
|
|
118
|
-
...config.thinking === "disabled" || config.reasoningEffort !== void 0 ? {} : { reasoning_effort: value.effort }
|
|
169
|
+
...config.thinking === "disabled" || config.reasoningEffort !== void 0 ? {} : { reasoning_effort: publicReasoningEffort(value.effort) }
|
|
119
170
|
})
|
|
120
171
|
})
|
|
121
172
|
}
|
|
122
|
-
}), 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 }) => {
|
|
123
174
|
const tools = toChatTools(options.tools), settings = requestSettings(config, cacheKey, policy);
|
|
124
175
|
return {
|
|
125
176
|
model: config.model,
|
|
126
177
|
...settings.sampling,
|
|
127
|
-
messages: toChatMessages(options.prompt),
|
|
178
|
+
messages: toChatMessages(options.prompt, { images: config.images ?? !1 }),
|
|
128
179
|
stream: streaming,
|
|
129
180
|
...options.responseFormat.type === "json" ? { response_format: {
|
|
130
181
|
type: "json_schema",
|
|
@@ -134,68 +185,84 @@ const requestSettings = (config, cacheKey, policy) => ({
|
|
|
134
185
|
...settings.extensions,
|
|
135
186
|
...tools.length > 0 ? { tools, tool_choice: toToolChoice(options.toolChoice) } : {}
|
|
136
187
|
};
|
|
137
|
-
})), postChat = (config, method, body, buffered = !1) => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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)));
|
|
154
216
|
}, failStatus = (config, method, res) => Effect.gen(function* () {
|
|
155
217
|
const text = yield* Effect.tryPromise({
|
|
156
218
|
try: () => res.text(),
|
|
157
219
|
catch: (e) => aiUnknown(config.moduleName, method, e)
|
|
158
|
-
}),
|
|
159
|
-
if (Option.isSome(semantic))
|
|
160
|
-
return yield* Effect.fail(semantic.value);
|
|
161
|
-
const headers = {};
|
|
220
|
+
}), headers = {};
|
|
162
221
|
res.headers.forEach((value, headerName) => {
|
|
163
222
|
headers[headerName] = value;
|
|
164
223
|
});
|
|
165
224
|
return yield* Effect.fail(AiError.make({
|
|
166
225
|
module: config.moduleName,
|
|
167
226
|
method,
|
|
168
|
-
reason:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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)
|
|
172
231
|
})
|
|
173
232
|
}));
|
|
174
233
|
});
|
|
175
|
-
export const makeCompatLanguageModel = (config) =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
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>;
|