@xandreed/plugin-models 0.14.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 +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
@@ -0,0 +1,73 @@
1
+ import { Effect, Ref, Schema, Stream } from "effect";
2
+ import { AiError } from "effect/ai";
3
+ import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/http";
4
+ import type { HttpClientRequest } from "effect/http";
5
+ import type { ModelCallMethod, ModelSelection, TurnError } from "@xandreed/core";
6
+ import { ModelRequestState, ModelRetryPolicy } from "../llm/model-retry-policy.entity.js";
7
+ import type { ProviderFailureProtocol } from "../llm/provider-failure.entity.js";
8
+ import type { ModelAttempt, ModelCompletionCall } from "../llm/model-completion.adapter.js";
9
+ export type ResilientHttpError = HttpClientError.HttpClientError | AiError.AiError | TurnError;
10
+ type BodyReaders = "json" | "text" | "arrayBuffer" | "urlParamsBody" | "formData" | "stream";
11
+ /** Exact body-reader error channels before the existing SDK-boundary adaptation. */
12
+ export type ResilientHttpResponse = Omit<HttpClientResponse.HttpClientResponse, BodyReaders> & {
13
+ readonly json: Effect.Effect<Schema.Json, ResilientHttpError>;
14
+ readonly text: Effect.Effect<string, ResilientHttpError>;
15
+ readonly arrayBuffer: Effect.Effect<ArrayBuffer, ResilientHttpError>;
16
+ readonly urlParamsBody: Effect.Effect<UrlParams.UrlParams, ResilientHttpError>;
17
+ readonly formData: Effect.Effect<FormData, ResilientHttpError>;
18
+ readonly stream: Stream.Stream<Uint8Array, ResilientHttpError>;
19
+ readonly modelAttempt: {
20
+ readonly call: ModelRequestCall;
21
+ readonly ordinal: number;
22
+ readonly completion: ModelAttempt;
23
+ };
24
+ };
25
+ /** One call's ephemeral identity and observable physical-attempt state; no stored ledger. */
26
+ export interface ModelRequestCall {
27
+ readonly policy: ModelRetryPolicy;
28
+ readonly completion: ModelCompletionCall;
29
+ readonly startedAt: number;
30
+ readonly deadline: number;
31
+ readonly reserveFallback: boolean;
32
+ readonly state: Ref.Ref<ModelRequestState>;
33
+ }
34
+ export declare const makeModelRequestCall: (options: {
35
+ readonly reserveFallback: boolean;
36
+ }) => Effect.Effect<ModelRequestCall, AiError.AiError, never>;
37
+ export declare const modelRequestSnapshot: (call: ModelRequestCall) => Effect.Effect<ModelRequestState>;
38
+ export declare const enterModelFallback: (call: ModelRequestCall, error: unknown) => Effect.Effect<boolean, never, never>;
39
+ /**
40
+ * A handed-off response with its body observed as it passes. A response
41
+ * this client handed off stays guarded: its readers keep their exact typed
42
+ * failures (timeouts, lost connections) and its model attempt. Any other
43
+ * response is rebuilt over a Web body.
44
+ */
45
+ export declare const observedResponse: (response: HttpClientResponse.HttpClientResponse, observe: (bytes: Uint8Array) => Effect.Effect<void>) => Effect.Effect<HttpClientResponse.HttpClientResponse | ResilientHttpResponse>;
46
+ /**
47
+ * How one request is sent: its error vocabulary, whether its response is
48
+ * settled (headers arrive only once the whole response is generated; absent:
49
+ * every call method but a stream's is settled) and the caller's admission
50
+ * before each physical send.
51
+ */
52
+ export interface ResilientRequestOptions {
53
+ readonly protocol: ProviderFailureProtocol;
54
+ readonly settled?: boolean;
55
+ readonly beforeSend?: Effect.Effect<void, AiError.AiError>;
56
+ }
57
+ /**
58
+ * One retry owner before response handoff; successful response readers
59
+ * retain exact typed failures. A streamed request waits for its headers at
60
+ * most `requestTimeoutMs`; a settled one's headers come with the whole
61
+ * generation, so they wait for the call's deadline and a timeout there is
62
+ * final instead of a second generation.
63
+ */
64
+ export declare const resilientRequest: <R>(client: HttpClient.HttpClient.With<HttpClientError.HttpClientError, R>, request: HttpClientRequest.HttpClientRequest, options: ResilientRequestOptions) => Effect.Effect<ResilientHttpResponse, ResilientHttpError, R>;
65
+ export declare const resilientHttpClient: <R>(client: HttpClient.HttpClient.With<HttpClientError.HttpClientError, R>, options: {
66
+ readonly protocol: ProviderFailureProtocol;
67
+ readonly adaptResponse: (response: ResilientHttpResponse) => HttpClientResponse.HttpClientResponse;
68
+ }) => HttpClient.HttpClient.With<ResilientHttpError, R>;
69
+ /** One provider invocation owns cleanup of a handed-off body that failed before raw decoding. */
70
+ export declare const withModelRequestCall: <A, E, R>(method: ModelCallMethod, selection: ModelSelection, operation: Effect.Effect<A, E, R>) => Effect.Effect<A, E | AiError.AiError | TurnError, R>;
71
+ /** Fixed SDK streams bridge an original recorder TurnError as a defect; no data/error relabeling. */
72
+ export declare const withModelRequestStream: <A, E, R>(method: ModelCallMethod, selection: ModelSelection, stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E | AiError.AiError | TurnError, R>;
73
+ export {};
@@ -0,0 +1,177 @@
1
+ import { Clock, Duration, Effect, Exit, Inspectable, Match, Option, Random, Ref, Schema, Stream } from "effect";
2
+ import { AiError } from "effect/ai";
3
+ import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/http";
4
+ import { isTurnError, modelFailureMessageOf, modelFailureOf, stampAiErrorFailure } from "@xandreed/core";
5
+ import { ModelRequestState, ModelRetryPolicy } from "../llm/model-retry-policy.entity.js";
6
+ import { enterFallbackState, retryableModelFailure, retryDelayMillis, retryHintMillis, startAttemptState } from "../llm/model-retry-policy.entity.functions.js";
7
+ import { CurrentModelRequestCall, CurrentModelRetryPolicy } from "../ports/model-retry-policy.port.js";
8
+ import { ProviderErrorBody } from "../llm/provider-failure.entity.js";
9
+ import { classifyAiError } from "../llm/provider-failure.entity.functions.js";
10
+ import { classifyLlmError } from "../llm/retry.js";
11
+ import { attachModelAttempt, beginModelAttempt, completeModelAttempt, completePendingAttempts, makeCompletionCall, observeModelBody, retainModelAttempt } from "../llm/model-completion.adapter.js";
12
+ import { CurrentModelCallMetadataOwner, CurrentModelCallMethod, CurrentModelCallSelection } from "../ports/model-call-completion.port.js";
13
+ const timeoutFailure = () => stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.UnknownError({ description: modelFailureMessageOf("Timeout") }) }), "Timeout");
14
+ export const makeModelRequestCall = Effect.fnUntraced(function* (options) {
15
+ const policy = yield* Schema.decodeUnknownEffect(ModelRetryPolicy)(yield* CurrentModelRetryPolicy, { onExcessProperty: "error" }).pipe(Effect.mapError(() => stampAiErrorFailure(AiError.make({ module: "Models", method: "requestPolicy", reason: new AiError.InvalidRequestError({ description: modelFailureMessageOf("InvalidRequest") }) }), "InvalidRequest"))), startedAt = yield* Clock.currentTimeMillis, state = yield* Ref.make({ _tag: "Ready", rung: "Primary", attempts: 0 });
16
+ return { policy, completion: yield* makeCompletionCall(), startedAt, deadline: startedAt + policy.totalTimeoutMs, reserveFallback: options.reserveFallback, state };
17
+ }), modelRequestSnapshot = (call) => Ref.get(call.state), enterModelFallback = Effect.fnUntraced(function* (call, error) {
18
+ const now = yield* Clock.currentTimeMillis;
19
+ if (now >= call.deadline || classifyLlmError(error, now) !== "transient")
20
+ return !1;
21
+ return yield* Ref.modify(call.state, (state) => Option.match(enterFallbackState(state, call.policy, call.reserveFallback), {
22
+ onNone: () => [!1, state],
23
+ onSome: (next) => [!0, next]
24
+ }));
25
+ });
26
+ const timed = (effect, boundMs) => effect.pipe(Effect.timeoutOrElse({ duration: Duration.millis(boundMs), orElse: () => Effect.fail(timeoutFailure()) })), pendingRead = (effect, call, boundMs) => Effect.flatMap(Clock.currentTimeMillis, (now) => now >= call.deadline ? Effect.fail(timeoutFailure()) : timed(effect, Math.min(boundMs, call.deadline - now))), rejectedBody = (response, call) => Stream.transformPull(response.stream, (pull) => Effect.succeed(pendingRead(pull, call, call.policy.bodyIdleTimeoutMs))), requestDetails = (request) => {
27
+ const url = URL.canParse(request.url) ? new URL(request.url) : new URL("https://provider.invalid/");
28
+ return { method: request.method, url: `${url.protocol}//${url.host}${url.pathname}`, urlParams: [], headers: {} };
29
+ }, bodyReadFailure = (response) => AiError.make({ module: "Models", method: "request", reason: new AiError.NetworkError({ reason: "TransportError", request: requestDetails(response.request), description: modelFailureMessageOf("Transient") }) }), committedBody = (response, call) => Stream.transformPull(response.stream, (pull) => Effect.succeed(timed(pull, call.policy.bodyIdleTimeoutMs))).pipe(Stream.mapError((error) => HttpClientError.isHttpClientError(error) && error.reason._tag === "DecodeError" ? bodyReadFailure(response) : error)), concatenate = (pieces) => {
30
+ const bytes = new Uint8Array(pieces.reduce((size, piece) => size + piece.byteLength, 0));
31
+ pieces.reduce((offset, piece) => {
32
+ bytes.set(piece, offset);
33
+ return offset + piece.byteLength;
34
+ }, 0);
35
+ return bytes;
36
+ };
37
+
38
+ class GuardedResponse {
39
+ stream;
40
+ buffered;
41
+ modelAttempt;
42
+ [HttpClientResponse.TypeId] = HttpClientResponse.TypeId;
43
+ ["~effect/http/HttpIncomingMessage"] = "~effect/http/HttpIncomingMessage";
44
+ #original;
45
+ constructor(original, stream, buffered, modelAttempt) {
46
+ this.stream = stream;
47
+ this.buffered = buffered;
48
+ this.modelAttempt = modelAttempt;
49
+ this.#original = original;
50
+ }
51
+ get request() {
52
+ return this.#original.request;
53
+ }
54
+ get url() {
55
+ return this.#original.url;
56
+ }
57
+ get status() {
58
+ return this.#original.status;
59
+ }
60
+ get headers() {
61
+ return this.#original.headers;
62
+ }
63
+ get cookies() {
64
+ return this.#original.cookies;
65
+ }
66
+ get remoteAddress() {
67
+ return this.#original.remoteAddress;
68
+ }
69
+ get upgrade() {
70
+ return this.#original.upgrade;
71
+ }
72
+ get pipe() {
73
+ return this.#original.pipe;
74
+ }
75
+ get arrayBuffer() {
76
+ return Effect.map(this.buffered, (bytes) => bytes.buffer);
77
+ }
78
+ get text() {
79
+ return Effect.map(this.buffered, (bytes) => new TextDecoder().decode(bytes));
80
+ }
81
+ get json() {
82
+ return this.text.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))), Effect.mapError((cause) => AiError.isAiError(cause) || HttpClientError.isHttpClientError(cause) || isTurnError(cause) ? cause : this.decodeFailure(cause)));
83
+ }
84
+ get urlParamsBody() {
85
+ return Effect.map(this.text, (text) => UrlParams.fromInput(new URLSearchParams(text)));
86
+ }
87
+ get formData() {
88
+ return this.buffered.pipe(Effect.flatMap((bytes) => Effect.tryPromise({ try: () => new Response(bytes, { headers: this.headers }).formData(), catch: (cause) => this.decodeFailure(cause) })));
89
+ }
90
+ decodeFailure(cause) {
91
+ return new HttpClientError.HttpClientError({ reason: new HttpClientError.DecodeError({ request: this.request, response: this.#original, cause, description: modelFailureMessageOf("InvalidResponse") }) });
92
+ }
93
+ toJSON() {
94
+ return this.#original.toJSON();
95
+ }
96
+ toString() {
97
+ return this.#original.toString();
98
+ }
99
+ [Inspectable.NodeInspectSymbol]() {
100
+ return this.toJSON();
101
+ }
102
+ }
103
+ const guardedResponse = Effect.fnUntraced(function* (response, stream, modelAttempt) {
104
+ const buffered = yield* Effect.cached(Stream.runCollect(stream).pipe(Effect.map(concatenate)));
105
+ return attachModelAttempt(modelAttempt.completion, new GuardedResponse(response, stream, buffered, modelAttempt));
106
+ }), wrapResponse = (response, call, ordinal, completion) => guardedResponse(response, committedBody(response, call), { call, ordinal, completion });
107
+ export const observedResponse = (response, observe) => response instanceof GuardedResponse ? guardedResponse(response, response.stream.pipe(Stream.tap(observe)), response.modelAttempt) : Effect.map(Stream.toReadableStreamEffect(response.stream.pipe(Stream.tap(observe))), (body) => retainModelAttempt(response, HttpClientResponse.fromWeb(response.request, new Response(body, { status: response.status, headers: new Headers(Object.entries(response.headers)) }))));
108
+ const httpDetails = (response, body) => ({ request: requestDetails(response.request), response: { status: response.status, headers: Object.fromEntries(Object.entries(response.headers).filter(([key]) => ["request-id", "x-request-id", "retry-after", "retry-after-ms"].includes(key))) }, body }), responseFailure = (response, text, protocol) => {
109
+ const http = httpDetails(response, text), classified = classifyAiError(AiError.make({ module: "Models", method: "request", reason: new AiError.UnknownError({ http }) }), protocol), kind = Option.getOrElse(modelFailureOf(classified), () => "Unknown"), diagnostic = Option.flatMap(Schema.decodeUnknownOption(Schema.fromJsonString(ProviderErrorBody))(text), (body) => Option.fromNullishOr([
110
+ typeof body.error === "string" ? body.error : body.error?.message,
111
+ body.message
112
+ ].find((value) => typeof value === "string" && value.trim().length > 0))), common = { http, description: Option.match(diagnostic, { onNone: () => modelFailureMessageOf(kind), onSome: (message) => message.slice(0, 500) }) }, reason = Match.value(kind).pipe(Match.when("Authentication", () => new AiError.AuthenticationError({ ...common, kind: response.status === 403 ? "InsufficientPermissions" : "InvalidKey" })), Match.when("Quota", () => new AiError.QuotaExhaustedError(common)), Match.when("RateLimit", () => new AiError.RateLimitError(common)), Match.when("InvalidRequest", () => new AiError.InvalidRequestError(common)), Match.when("ContextOverflow", () => new AiError.InvalidRequestError(common)), Match.when("InvalidResponse", () => new AiError.InvalidResponseError(common)), Match.when("Transient", () => new AiError.InternalProviderError(common)), Match.orElse(() => new AiError.UnknownError(common)));
113
+ return stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason }), kind);
114
+ }, errorBody = Effect.fnUntraced(function* (response, call, attempt) {
115
+ const bytes = new Uint8Array(65536), retained = yield* Ref.make(0), size = yield* Stream.runFoldEffect(rejectedBody(response, call), () => 0, (size, piece) => {
116
+ const keep = Math.min(piece.byteLength, bytes.byteLength - size);
117
+ bytes.set(piece.subarray(0, keep), size);
118
+ return size + piece.byteLength > bytes.byteLength ? Effect.fail(stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.InvalidResponseError({ description: modelFailureMessageOf("InvalidResponse"), http: httpDetails(response, new TextDecoder().decode(bytes)) }) }), "InvalidResponse")) : Ref.set(retained, size + piece.byteLength).pipe(Effect.as(size + piece.byteLength));
119
+ }).pipe(Effect.tap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !0)), Effect.catchTag("HttpClientError", (error) => ["DecodeError", "EmptyBodyError", "TransportError"].includes(error.reason._tag) ? Ref.get(retained).pipe(Effect.tap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !1))) : Effect.fail(error)), Effect.tapError(() => Ref.get(retained).pipe(Effect.flatMap((size) => observeModelBody(attempt, new TextDecoder().decode(bytes.subarray(0, size)), !1)))));
120
+ return new TextDecoder().decode(bytes.subarray(0, size));
121
+ }), errorKind = (error) => HttpClientError.isHttpClientError(error) ? Match.valueTags(error.reason, {
122
+ TransportError: () => "Transient",
123
+ EncodeError: () => "InvalidRequest",
124
+ InvalidUrlError: () => "InvalidRequest",
125
+ StatusCodeError: () => "Unknown",
126
+ DecodeError: () => "InvalidResponse",
127
+ EmptyBodyError: () => "InvalidResponse"
128
+ }) : Option.getOrElse(modelFailureOf(error), () => "Unknown"), replayableBody = (request) => Match.valueTags(request.body, {
129
+ Empty: () => !0,
130
+ Uint8Array: () => !0,
131
+ Stream: () => !1,
132
+ FormData: () => !1,
133
+ Raw: (body) => typeof body.body === "string" || body.body instanceof ArrayBuffer || ArrayBuffer.isView(body.body)
134
+ }), awaitReady = Effect.fnUntraced(function* (call) {
135
+ const now = yield* Clock.currentTimeMillis;
136
+ if (now >= call.deadline)
137
+ return yield* Effect.fail(timeoutFailure());
138
+ const state = yield* Ref.get(call.state), readyAt = ModelRequestState.match(state, { Ready: () => now, Rejected: (value) => value.retryAt ?? now, Sending: () => now, Committed: () => now });
139
+ if (readyAt > now)
140
+ yield* Effect.sleep(Duration.millis(Math.min(readyAt - now, call.deadline - now)));
141
+ if ((yield* Clock.currentTimeMillis) >= call.deadline)
142
+ return yield* Effect.fail(timeoutFailure());
143
+ }), requestCall = Effect.fnUntraced(function* () {
144
+ const inherited = yield* CurrentModelRequestCall;
145
+ if (Option.isSome(inherited) && (yield* Ref.get(inherited.value.state))._tag !== "Committed")
146
+ return inherited.value;
147
+ return yield* makeModelRequestCall({ reserveFallback: !1 });
148
+ });
149
+ export const resilientRequest = Effect.fn("models.request")(function* (client, request, options) {
150
+ const call = yield* requestCall(), replayable = replayableBody(request), headerBound = options.settled ?? (yield* CurrentModelCallMethod) !== "Stream" ? call.policy.totalTimeoutMs : call.policy.requestTimeoutMs, run = (previous) => Effect.gen(function* () {
151
+ yield* awaitReady(call);
152
+ yield* options.beforeSend ?? Effect.void;
153
+ const started = yield* Ref.modify(call.state, (state) => Option.match(startAttemptState(state, call.policy, call.reserveFallback), { onNone: () => [Option.none(), state], onSome: (next) => [Option.some(next.attempts), next] }));
154
+ if (Option.isNone(started))
155
+ return yield* Option.match(previous, { onNone: () => Effect.fail(stampAiErrorFailure(AiError.make({ module: "Models", method: "request", reason: new AiError.InvalidRequestError({ description: modelFailureMessageOf("InvalidRequest") }) }), "InvalidRequest")), onSome: Effect.fail });
156
+ const ordinal = started.value, attempt = yield* beginModelAttempt(call.completion, ordinal, options.protocol), execute = client.execute(request).pipe(Effect.provideService(CurrentModelRequestCall, Option.some(call)), Effect.catchTag("HttpClientError", (error) => error.reason._tag === "StatusCodeError" ? Effect.succeed(error.reason.response) : Effect.fail(error))), result = yield* Effect.result(pendingRead(execute, call, headerBound).pipe(Effect.flatMap((response) => response.status >= 200 && response.status < 300 ? Ref.update(call.state, (state) => state._tag === "Sending" && state.attempts === ordinal ? { _tag: "Committed", rung: state.rung, attempts: ordinal } : state).pipe(Effect.andThen(wrapResponse(response, call, ordinal, attempt))) : errorBody(response, call, attempt).pipe(Effect.flatMap((body) => Effect.fail(responseFailure(response, body, options.protocol)))))));
157
+ if (result._tag === "Success")
158
+ return result.success;
159
+ const failure = result.failure;
160
+ if (isTurnError(failure))
161
+ return yield* Effect.fail(failure);
162
+ const kind = errorKind(failure);
163
+ yield* completeModelAttempt(attempt, { _tag: "Failed", failure: Option.some(kind) });
164
+ const now = yield* Clock.currentTimeMillis, hint = AiError.isAiError(failure) && "http" in failure.reason ? retryHintMillis(failure.reason.http?.response?.headers ?? {}, now) : Option.none(), delay = retryDelayMillis(call.policy, ordinal, yield* Random.next, hint);
165
+ yield* Ref.update(call.state, (state) => state._tag === "Sending" && state.attempts === ordinal ? { _tag: "Rejected", rung: state.rung, attempts: ordinal, kind, replayable, ...retryableModelFailure(kind) && replayable ? { retryAt: now + delay } : {} } : state);
166
+ const state = yield* Ref.get(call.state);
167
+ return Option.isSome(startAttemptState(state, call.policy, call.reserveFallback)) ? yield* run(Option.some(failure)) : yield* Effect.fail(failure);
168
+ });
169
+ return yield* run(Option.none()).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? completePendingAttempts(call.completion, exit) : Effect.void));
170
+ }), resilientHttpClient = (client, options) => HttpClient.makeWith((prepared) => Effect.flatMap(prepared, (request) => resilientRequest(HttpClient.makeWith(client.postprocess, Effect.succeed), request, options).pipe(Effect.map(options.adaptResponse))), client.preprocess), withModelRequestCall = (method, selection, operation) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
171
+ const call = yield* requestCall(), inherited = Option.contains(yield* CurrentModelCallMetadataOwner, call.completion.callId), effectiveMethod = inherited ? yield* CurrentModelCallMethod : method, effectiveSelection = inherited ? yield* CurrentModelCallSelection : Option.some(selection), result = yield* Effect.exit(restore(operation.pipe(Effect.provideService(CurrentModelRequestCall, Option.some(call)), Effect.provideService(CurrentModelCallMethod, effectiveMethod), Effect.provideService(CurrentModelCallSelection, effectiveSelection), Effect.provideService(CurrentModelCallMetadataOwner, Option.some(call.completion.callId)))));
172
+ yield* completePendingAttempts(call.completion, result);
173
+ return yield* Exit.match(result, { onSuccess: Effect.succeed, onFailure: Effect.failCause });
174
+ })), withModelRequestStream = (method, selection, stream) => Stream.unwrap(Effect.gen(function* () {
175
+ const call = yield* requestCall(), inherited = Option.contains(yield* CurrentModelCallMetadataOwner, call.completion.callId), effectiveMethod = inherited ? yield* CurrentModelCallMethod : method, effectiveSelection = inherited ? yield* CurrentModelCallSelection : Option.some(selection);
176
+ return stream.pipe(Stream.provideService(CurrentModelRequestCall, Option.some(call)), Stream.provideService(CurrentModelCallMethod, effectiveMethod), Stream.provideService(CurrentModelCallSelection, effectiveSelection), Stream.provideService(CurrentModelCallMetadataOwner, Option.some(call.completion.callId)), Stream.onEnd(completePendingAttempts(call.completion, Exit.succeed(void 0))), Stream.onExit((exit) => completePendingAttempts(call.completion, Exit.isSuccess(exit) ? Exit.interrupt() : exit).pipe(Effect.catch((error) => Effect.die(error)))));
177
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xandreed/plugin-models",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "engines": {
5
5
  "node": ">=24.5",
6
6
  "bun": ">=1.3.0"
@@ -13,13 +13,15 @@
13
13
  "default": "./dist/index.js"
14
14
  },
15
15
  "./compat": {
16
- "types": "./dist/llm/compat.d.ts",
17
- "default": "./dist/llm/compat.js"
16
+ "types": "./dist/compat/index.d.ts",
17
+ "default": "./dist/compat/index.js"
18
+ },
19
+ "./conformance": {
20
+ "types": "./dist/conformance/index.d.ts",
21
+ "default": "./dist/conformance/index.js"
18
22
  }
19
23
  },
20
24
  "dependencies": {
21
- "@xandreed/core": "0.14.0",
22
- "effect": "4.0.7",
23
25
  "@effect/ai-anthropic": "4.0.7",
24
26
  "@effect/ai-openai": "4.0.7",
25
27
  "undici": "^7.30.1",
@@ -27,6 +29,10 @@
27
29
  },
28
30
  "private": false,
29
31
  "types": "./dist/index.d.ts",
32
+ "peerDependencies": {
33
+ "@xandreed/core": "^0.15.0",
34
+ "effect": "^4.0.7"
35
+ },
30
36
  "files": [
31
37
  "dist",
32
38
  "skills",
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,33 +0,0 @@
1
- /** One request a `RecordingProxy` received: a CONNECT tunnel (`host:port`) or an absolute-form request (its URL). */
2
- export interface ProxiedRequest {
3
- readonly kind: "connect" | "absolute";
4
- readonly target: string;
5
- readonly authorization: string | null;
6
- }
7
- export interface RecordingProxy {
8
- readonly url: string;
9
- readonly port: number;
10
- readonly seen: ReadonlyArray<ProxiedRequest>;
11
- readonly close: () => Promise<void>;
12
- }
13
- /**
14
- * A recording HTTP proxy on 127.0.0.1 for tests, over raw sockets (so no
15
- * environment proxy applies to it): it records every request, opens CONNECT
16
- * tunnels and forwards absolute-form requests to `*.invalid` hosts on this
17
- * machine (the port as asked), and refuses every other host with 403, so a
18
- * test never reaches the network. `reset: true` makes it close every
19
- * connection as soon as it opens; `authenticate: true` answers every request
20
- * with 407 (Proxy Authentication Required).
21
- */
22
- export declare const startRecordingProxy: (options?: {
23
- readonly reset?: boolean;
24
- readonly authenticate?: boolean;
25
- }) => Promise<RecordingProxy>;
26
- /**
27
- * Whether the machine running the tests has a proxy in its environment. Bun's
28
- * fetch applies one to every request without a `proxy` option, whatever a
29
- * test's `ConfigProvider` says, so the tests of direct requests skip then.
30
- */
31
- export declare const ambientProxy: boolean;
32
- /** A port on 127.0.0.1 that nothing listens on (bound, then released). */
33
- export declare const closedPort: () => Promise<number>;