@xandreed/plugin-models 0.14.0 → 0.15.1

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