@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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/anthropicOAuth.d.ts +45 -11
  3. package/dist/auth/anthropicOAuth.js +23 -23
  4. package/dist/auth/auth-store.conformance.d.ts +63 -0
  5. package/dist/auth/auth-store.conformance.js +152 -0
  6. package/dist/auth/credential-input.entity.d.ts +52 -0
  7. package/dist/auth/credential-input.entity.functions.d.ts +18 -0
  8. package/dist/auth/credential-input.entity.functions.js +12 -0
  9. package/dist/auth/credential-input.entity.js +22 -0
  10. package/dist/auth/credential-refresh.entity.d.ts +17 -0
  11. package/dist/auth/credential-refresh.entity.functions.d.ts +57 -0
  12. package/dist/auth/credential-refresh.entity.functions.js +21 -0
  13. package/dist/auth/credential-refresh.entity.js +2 -0
  14. package/dist/auth/localAuth.d.ts +12 -2
  15. package/dist/auth/localAuth.js +58 -86
  16. package/dist/auth/memory-auth.adapter.d.ts +35 -0
  17. package/dist/auth/memory-auth.adapter.js +35 -0
  18. package/dist/auth/openAiCodexOAuth.d.ts +28 -6
  19. package/dist/auth/openAiCodexOAuth.js +22 -32
  20. package/dist/auth/token-response.entity.d.ts +21 -0
  21. package/dist/auth/token-response.entity.functions.d.ts +20 -0
  22. package/dist/auth/token-response.entity.functions.js +10 -0
  23. package/dist/auth/token-response.entity.js +10 -0
  24. package/dist/compat/index.d.ts +2 -0
  25. package/dist/compat/index.js +1 -0
  26. package/dist/conformance/index.d.ts +6 -0
  27. package/dist/conformance/index.js +3 -0
  28. package/dist/index.d.ts +30 -13
  29. package/dist/index.js +25 -50
  30. package/dist/llm/chat-wire.entity.d.ts +123 -0
  31. package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
  32. package/dist/llm/chat-wire.entity.functions.js +10 -0
  33. package/dist/llm/chat-wire.entity.js +37 -0
  34. package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
  35. package/dist/llm/compat-telemetry.adapter.js +21 -0
  36. package/dist/llm/compat.d.ts +84 -33
  37. package/dist/llm/compat.js +188 -121
  38. package/dist/llm/model-completion.adapter.d.ts +49 -0
  39. package/dist/llm/model-completion.adapter.js +77 -0
  40. package/dist/llm/model-completion.entity.d.ts +4 -0
  41. package/dist/llm/model-completion.entity.functions.d.ts +3 -0
  42. package/dist/llm/model-completion.entity.functions.js +7 -0
  43. package/dist/llm/model-completion.entity.js +7 -0
  44. package/dist/llm/model-retry-policy.entity.d.ts +37 -0
  45. package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
  46. package/dist/llm/model-retry-policy.entity.functions.js +33 -0
  47. package/dist/llm/model-retry-policy.entity.js +23 -0
  48. package/dist/llm/model-settings.entity.d.ts +85 -0
  49. package/dist/llm/model-settings.entity.functions.d.ts +11 -0
  50. package/dist/llm/model-settings.entity.functions.js +49 -0
  51. package/dist/llm/model-settings.entity.js +28 -0
  52. package/dist/llm/model-usage.entity.d.ts +61 -0
  53. package/dist/llm/model-usage.entity.functions.d.ts +11 -0
  54. package/dist/llm/model-usage.entity.functions.js +24 -0
  55. package/dist/llm/model-usage.entity.js +19 -0
  56. package/dist/llm/modelCatalog.d.ts +47 -7
  57. package/dist/llm/modelCatalog.js +31 -57
  58. package/dist/llm/openAiCodex.d.ts +18 -7
  59. package/dist/llm/openAiCodex.js +57 -45
  60. package/dist/llm/openAiCodexWebSocket.d.ts +60 -7
  61. package/dist/llm/openAiCodexWebSocket.js +113 -105
  62. package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
  63. package/dist/llm/prompt-cache-frame.entity.js +10 -0
  64. package/dist/llm/prompt-cache.entity.d.ts +57 -0
  65. package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
  66. package/dist/llm/prompt-cache.entity.functions.js +73 -0
  67. package/dist/llm/prompt-cache.entity.js +27 -0
  68. package/dist/llm/provider-client.adapter.d.ts +9 -0
  69. package/dist/llm/provider-client.adapter.js +60 -0
  70. package/dist/llm/provider-failure.entity.d.ts +42 -0
  71. package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
  72. package/dist/llm/provider-failure.entity.functions.js +67 -0
  73. package/dist/llm/provider-failure.entity.js +20 -0
  74. package/dist/llm/provider-profile.entity.d.ts +79 -0
  75. package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
  76. package/dist/llm/provider-profile.entity.functions.js +83 -0
  77. package/dist/llm/provider-profile.entity.js +10 -0
  78. package/dist/llm/providers.d.ts +125 -29
  79. package/dist/llm/providers.js +311 -117
  80. package/dist/llm/retry.d.ts +11 -47
  81. package/dist/llm/retry.js +33 -52
  82. package/dist/llm/router.d.ts +56 -28
  83. package/dist/llm/router.js +125 -88
  84. package/dist/llm/sse.d.ts +58 -29
  85. package/dist/llm/sse.js +47 -39
  86. package/dist/llm/thinking-binding.entity.d.ts +49 -0
  87. package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
  88. package/dist/llm/thinking-binding.entity.functions.js +16 -0
  89. package/dist/llm/thinking-binding.entity.js +11 -0
  90. package/dist/llm/utilityLlm.d.ts +14 -5
  91. package/dist/llm/utilityLlm.js +18 -13
  92. package/dist/plugin.adapter.d.ts +154 -6
  93. package/dist/plugin.adapter.js +53 -10
  94. package/dist/ports/auth-store.port.d.ts +73 -0
  95. package/dist/ports/auth-store.port.js +24 -0
  96. package/dist/ports/host-credentials.port.d.ts +22 -0
  97. package/dist/ports/host-credentials.port.js +4 -0
  98. package/dist/ports/model-call-completion.port.d.ts +9 -0
  99. package/dist/ports/model-call-completion.port.js +2 -0
  100. package/dist/ports/model-endpoints.port.d.ts +66 -19
  101. package/dist/ports/model-endpoints.port.js +22 -4
  102. package/dist/ports/model-retry-policy.port.d.ts +17 -0
  103. package/dist/ports/model-retry-policy.port.js +3 -0
  104. package/dist/ports/model-settings.port.d.ts +32 -0
  105. package/dist/ports/model-settings.port.js +4 -0
  106. package/dist/ports/models-transport.port.d.ts +53 -0
  107. package/dist/ports/models-transport.port.js +7 -0
  108. package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
  109. package/dist/ports/prompt-cache-frame.port.js +4 -0
  110. package/dist/ports/settings-store.port.d.ts +70 -0
  111. package/dist/ports/settings-store.port.js +33 -0
  112. package/dist/ports/thinking-binding.port.d.ts +7 -0
  113. package/dist/ports/thinking-binding.port.js +4 -0
  114. package/dist/settings/localSettings.d.ts +2 -1
  115. package/dist/settings/localSettings.js +18 -61
  116. package/dist/settings/memory-settings.adapter.d.ts +12 -0
  117. package/dist/settings/memory-settings.adapter.js +11 -0
  118. package/dist/settings/roleView.d.ts +3 -1
  119. package/dist/settings/roleView.js +1 -1
  120. package/dist/settings/settings-store.conformance.d.ts +47 -0
  121. package/dist/settings/settings-store.conformance.js +100 -0
  122. package/dist/settings/settings-value.entity.d.ts +15 -0
  123. package/dist/settings/settings-value.entity.functions.d.ts +51 -0
  124. package/dist/settings/settings-value.entity.functions.js +42 -0
  125. package/dist/settings/settings-value.entity.js +2 -0
  126. package/dist/storage/json-file.d.ts +18 -0
  127. package/dist/storage/json-file.js +26 -0
  128. package/dist/transport/http-proxy.entity.d.ts +104 -0
  129. package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
  130. package/dist/transport/http-proxy.entity.functions.js +112 -0
  131. package/dist/transport/http-proxy.entity.js +42 -0
  132. package/dist/transport/model-request.conformance.d.ts +13 -0
  133. package/dist/transport/model-request.conformance.js +36 -0
  134. package/dist/transport/models-transport.adapter.d.ts +49 -0
  135. package/dist/transport/models-transport.adapter.js +99 -0
  136. package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
  137. package/dist/transport/resilient-http-client.adapter.js +177 -0
  138. package/dist/transport/undici-fetch.adapter.d.ts +13 -0
  139. package/dist/transport/undici-fetch.adapter.js +1 -0
  140. package/package.json +13 -6
  141. package/dist/auth/anthropicOAuth.test.d.ts +0 -1
  142. package/dist/auth/localAuth.test.d.ts +0 -1
  143. package/dist/llm/compat.test.d.ts +0 -1
  144. package/dist/llm/modelCatalog.test.d.ts +0 -1
  145. package/dist/llm/providers.test.d.ts +0 -1
  146. package/dist/llm/retry.test.d.ts +0 -1
  147. package/dist/llm/router.test.d.ts +0 -1
  148. package/dist/llm/sse.test.d.ts +0 -1
  149. package/dist/plugin.adapter.test.d.ts +0 -1
  150. package/dist/settings/localSettings.test.d.ts +0 -1
  151. package/dist/settings/roleView.test.d.ts +0 -1
@@ -1,59 +1,113 @@
1
- import { AiError, LanguageModel, Tool } from "effect/ai";
2
- import { Effect, Result, Option, Stream } from "effect";
3
- import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, strictJsonSchema, toolParametersSchema } from "@xandreed/core";
4
- import { finishReasonFromWire, sseStreamParts, usageFromCompletion } from "./sse.js";
5
- const record = (value) => typeof value === "object" && value !== null ? value : {}, semanticGatewayError = (moduleName, method, body) => Option.flatMap(Option.fromNullishOr(Result.getOrUndefined(Result.try(() => JSON.parse(body)))), (decoded) => {
6
- const error = record(record(decoded).error), type = typeof error.type === "string" ? error.type : "", message = typeof error.message === "string" ? error.message : "";
7
- if (type === "ModelError" || /model .*not supported/i.test(message))
8
- return Option.some(AiError.make({
9
- module: moduleName,
10
- method,
11
- reason: new AiError.InvalidRequestError({
12
- description: message.length > 0 ? message : "the selected model is not supported"
13
- })
14
- }));
15
- if (type === "CreditsError" || /insufficient (?:balance|credits)|usage limit|quota/i.test(message))
16
- return Option.some(AiError.make({
17
- module: moduleName,
18
- method,
19
- reason: new AiError.UnknownError({ description: `CreditsError: ${message || "provider quota exhausted"}` })
20
- }));
21
- return Option.none();
22
- }), aiUnknown = (moduleName, method, e) => AiError.make({ module: moduleName, method, reason: new AiError.UnknownError({ description: String(e) }) }), invalidOutput = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidOutputError({ description }) }), requestInfo = (chatUrl) => ({
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
- export const toChatMessages = (prompt) => prompt.content.flatMap((message) => {
29
- if (message.role === "system")
30
- return [{ role: "system", content: message.content }];
31
- if (message.role === "user")
32
- return [{ role: "user", content: message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join("") }];
33
- if (message.role === "assistant") {
34
- const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), toolCalls = message.content.flatMap((p) => p.type === "tool-call" ? [
35
- {
36
- id: p.id,
37
- type: "function",
38
- function: { name: p.name, arguments: JSON.stringify(p.params ?? {}) }
39
- }
40
- ] : []);
41
- return [
42
- {
43
- role: "assistant",
44
- content: text.length > 0 ? text : null,
45
- ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
46
- }
47
- ];
48
- }
49
- return message.content.flatMap((p) => p.type === "tool-result" ? [
50
- {
51
- role: "tool",
52
- tool_call_id: p.id,
53
- content: typeof p.result === "string" ? p.result : JSON.stringify(p.result ?? null)
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
- }), toChatTools = (tools) => tools.flatMap((tool) => Tool.isUserDefined(tool) ? [
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" && choice !== null && "tool" in choice)
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) => Effect.gen(function* () {
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(invalidOutput(moduleName, "generateText", "the completion carried no choices"));
88
- const message = choice.message ?? {}, reasoning = message.reasoning_content ?? message.reasoning, text = message.content, toolCalls = yield* Effect.forEach(message.tool_calls ?? [], (tc) => Effect.try({
89
- try: () => ({
90
- type: "tool-call",
91
- id: tc.id ?? "",
92
- name: tc.function?.name ?? "",
93
- params: JSON.parse(tc.function?.arguments ?? "{}")
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: toolCalls.length > 0 ? "tool-calls" : finishReasonFromWire(choice.finish_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 request = (config.beforeRequest?.(body) ?? Effect.void).pipe(Effect.andThen(Effect.tryPromise({
139
- try: async (signal) => {
140
- const response = await (config.fetchImpl ?? fetch)(config.chatUrl, {
141
- signal,
142
- method: "POST",
143
- headers: {
144
- "content-type": "application/json",
145
- ...config.apiKey === void 0 || config.apiKey.length === 0 ? {} : { authorization: `Bearer ${config.apiKey}` }
146
- },
147
- body: JSON.stringify(body)
148
- });
149
- return buffered ? new Response(await response.arrayBuffer(), { status: response.status, statusText: response.statusText, headers: response.headers }) : response;
150
- },
151
- catch: (e) => aiUnknown(config.moduleName, method, e)
152
- })));
153
- return config.aroundRequest === void 0 ? request : config.aroundRequest(body, request);
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
- }), semantic = semanticGatewayError(config.moduleName, method, text);
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: AiError.reasonFromHttpStatus({
169
- status: res.status,
170
- http: { request: requestInfo(config.chatUrl), response: { status: res.status, headers }, body: text.slice(0, 500) },
171
- description: text.slice(0, 500)
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) => LanguageModel.make({
176
- generateText: (options) => Effect.gen(function* () {
177
- const res = yield* postChat(config, "generateText", yield* chatRequestBody(config, options, !1), !0);
178
- if (!res.ok)
179
- return yield* failStatus(config, "generateText", res);
180
- const text = yield* Effect.tryPromise({
181
- try: () => res.text(),
182
- catch: (e) => aiUnknown(config.moduleName, "generateText", e)
183
- }), parsed = yield* Effect.try({
184
- try: () => JSON.parse(text),
185
- catch: () => invalidOutput(config.moduleName, "generateText", `the completion body was not JSON: ${text.slice(0, 200)}`)
186
- });
187
- return yield* fromChatCompletion(config.moduleName, parsed);
188
- }),
189
- streamText: (options) => Stream.unwrap(Effect.gen(function* () {
190
- const res = yield* postChat(config, "streamText", yield* chatRequestBody(config, options, !0));
191
- if (!res.ok)
192
- return yield* failStatus(config, "streamText", res);
193
- const body = res.body;
194
- if (body === null)
195
- return yield* Effect.fail(invalidOutput(config.moduleName, "streamText", "the streaming response carried no body"));
196
- return sseStreamParts({ moduleName: config.moduleName, body });
197
- }))
198
- }).pipe(Effect.map((model) => describeModel(model, Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }).pipe(Effect.map(({ cacheKey, policy }) => {
199
- const settings = requestSettings(config, cacheKey, policy);
200
- return { provider: config.moduleName, model: config.model, settings: { ...settings.sampling, ...settings.extensions } };
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,3 @@
1
+ import { ModelCompletionState } from "./model-completion.entity.js";
2
+ /** Only the first terminal observation owns the durable append and its ACK. */
3
+ export declare const claimModelCompletion: (state: ModelCompletionState) => readonly [boolean, ModelCompletionState];
@@ -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>;