@opencode-ai/ai 0.0.0-dev-18409 → 0.0.0-dev-18412
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image.js +5 -4
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.js +4 -6
- package/dist/protocols/bedrock-event-stream.js +16 -6
- package/dist/protocols/gemini.d.ts +1 -0
- package/dist/protocols/gemini.js +13 -1
- package/dist/protocols/google-images.js +8 -18
- package/dist/protocols/open-responses-channel.js +20 -9
- package/dist/protocols/open-responses-continuation.js +9 -8
- package/dist/protocols/open-responses.d.ts +1 -1
- package/dist/protocols/open-responses.js +7 -16
- package/dist/protocols/openai-chat.js +23 -18
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +7 -3
- package/dist/protocols/shared.js +26 -13
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- package/dist/protocols/xai-images.js +7 -12
- package/dist/protocols/zai-images.js +5 -10
- package/dist/provider-error.d.ts +4 -4
- package/dist/provider-error.js +31 -32
- package/dist/route/auth.js +3 -5
- package/dist/route/client.js +29 -11
- package/dist/route/executor.d.ts +9 -5
- package/dist/route/executor.js +33 -66
- package/dist/route/framing.d.ts +2 -0
- package/dist/route/transport/http.js +7 -2
- package/dist/route/transport/index.d.ts +3 -1
- package/dist/route/transport/websocket-channel.d.ts +2 -1
- package/dist/route/transport/websocket.d.ts +2 -1
- package/dist/route/transport/websocket.js +70 -30
- package/dist/schema/errors.d.ts +71 -88
- package/dist/schema/errors.js +36 -85
- package/package.json +3 -3
package/dist/route/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
|
|
3
|
-
import { HttpContext, HttpRateLimitDetails,
|
|
3
|
+
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
|
|
4
4
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
5
5
|
export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {
|
|
6
6
|
}
|
|
@@ -60,20 +60,11 @@ const rateLimitDetails = (headers, retryAfter) => {
|
|
|
60
60
|
reset: Object.keys(reset).length === 0 ? undefined : reset,
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
url: request.url,
|
|
66
|
-
headers: headerDetails(request.headers),
|
|
67
|
-
});
|
|
68
|
-
const responseDetails = (response) => new HttpResponseDetails({
|
|
63
|
+
export const responseHttp = (response) => new HttpContext({
|
|
64
|
+
url: response.request.url,
|
|
69
65
|
status: response.status,
|
|
70
66
|
headers: headerDetails(response.headers),
|
|
71
67
|
});
|
|
72
|
-
const responseBody = (body) => {
|
|
73
|
-
if (body === undefined)
|
|
74
|
-
return {};
|
|
75
|
-
return { body };
|
|
76
|
-
};
|
|
77
68
|
const decodeProviderBody = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({
|
|
78
69
|
message: Schema.optionalKey(Schema.String),
|
|
79
70
|
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
|
|
@@ -83,60 +74,36 @@ const providerMessage = (status, body) => {
|
|
|
83
74
|
return ([decoded?.error?.message, decoded?.message].find((message) => message?.trim()) ??
|
|
84
75
|
`Provider request failed with HTTP ${status}`);
|
|
85
76
|
};
|
|
86
|
-
const
|
|
87
|
-
request: requestDetails(input.request),
|
|
88
|
-
response: responseDetails(input.response),
|
|
89
|
-
...input.body,
|
|
90
|
-
rateLimit: input.rateLimit,
|
|
91
|
-
});
|
|
92
|
-
const statusError = (request) => (response) => Effect.gen(function* () {
|
|
77
|
+
const statusError = (response) => Effect.gen(function* () {
|
|
93
78
|
if (response.status < 400)
|
|
94
79
|
return response;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
reason: classifyProviderFailure({
|
|
104
|
-
status: response.status,
|
|
105
|
-
message: providerMessage(response.status, body),
|
|
106
|
-
retryAfterMs: retryAfter,
|
|
107
|
-
rateLimit,
|
|
108
|
-
http: responseHttp({
|
|
109
|
-
request,
|
|
110
|
-
response,
|
|
111
|
-
body: details,
|
|
112
|
-
rateLimit,
|
|
113
|
-
}),
|
|
114
|
-
}),
|
|
80
|
+
const result = yield* response.text.pipe(Effect.result);
|
|
81
|
+
return yield* httpFailure({
|
|
82
|
+
message: providerMessage(response.status, result._tag === "Success" ? result.success : undefined),
|
|
83
|
+
url: response.request.url,
|
|
84
|
+
status: response.status,
|
|
85
|
+
responseHeaders: headerDetails(response.headers),
|
|
86
|
+
responseBody: result._tag === "Success" ? result.success : undefined,
|
|
87
|
+
cause: result._tag === "Failure" ? (result.failure.cause ?? result.failure) : undefined,
|
|
115
88
|
});
|
|
116
89
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// executor-driven requests produce. The originating request is not available on
|
|
120
|
-
// that path, so the method is assumed (language model calls are always POST),
|
|
121
|
-
// request headers are empty.
|
|
122
|
-
export const classifyHttpFailure = (input) => {
|
|
90
|
+
/** Preserve HTTP diagnostics for executor and externally captured failures alike. */
|
|
91
|
+
export const httpFailure = (input) => {
|
|
123
92
|
const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders));
|
|
124
93
|
const retryAfter = retryAfterMs(headers);
|
|
125
94
|
const rateLimit = rateLimitDetails(headers, retryAfter);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
http: new HttpContext({
|
|
134
|
-
request: new HttpRequestDetails({ method: "POST", url: input.url, headers: {} }),
|
|
135
|
-
response: input.status === undefined
|
|
136
|
-
? undefined
|
|
137
|
-
: new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }),
|
|
138
|
-
...details,
|
|
95
|
+
return new AIError({
|
|
96
|
+
reason: classifyProviderFailure({
|
|
97
|
+
message: input.message,
|
|
98
|
+
status: input.status,
|
|
99
|
+
data: input.data,
|
|
100
|
+
rawBody: input.responseBody,
|
|
101
|
+
retryAfterMs: retryAfter,
|
|
139
102
|
rateLimit,
|
|
103
|
+
cause: input.cause,
|
|
104
|
+
http: input.status === undefined || input.url === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: new HttpContext({ url: input.url, status: input.status, headers }),
|
|
140
107
|
}),
|
|
141
108
|
});
|
|
142
109
|
};
|
|
@@ -160,19 +127,18 @@ const nativeTransportFailure = (error) => {
|
|
|
160
127
|
const httpError = (input) => {
|
|
161
128
|
const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request;
|
|
162
129
|
const transportError = (failure) => new AIError({
|
|
163
|
-
|
|
164
|
-
method: input.operation,
|
|
165
|
-
reason: new TransportReason({
|
|
130
|
+
reason: new TransportError({
|
|
166
131
|
message: failure.message,
|
|
132
|
+
cause: source,
|
|
133
|
+
http: input.http,
|
|
167
134
|
transport: "http",
|
|
168
135
|
operation: input.operation,
|
|
169
136
|
code: failure.code,
|
|
170
137
|
url: request.url,
|
|
171
|
-
http: new HttpContext({ request: requestDetails(request) }),
|
|
172
138
|
}),
|
|
173
139
|
});
|
|
174
140
|
const source = HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason
|
|
175
|
-
? input.error.reason.cause
|
|
141
|
+
? (input.error.reason.cause ?? input.error)
|
|
176
142
|
: input.error;
|
|
177
143
|
const native = nativeTransportFailure(source);
|
|
178
144
|
const code = native?.code;
|
|
@@ -194,19 +160,20 @@ const httpError = (input) => {
|
|
|
194
160
|
code: code ?? input.error.reason._tag,
|
|
195
161
|
});
|
|
196
162
|
};
|
|
163
|
+
export const responseStream = (response) => response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read", http: responseHttp(response) })));
|
|
197
164
|
export const stream = (executor, request, middleware) => Stream.unwrap(Effect.gen(function* () {
|
|
198
165
|
const response = yield* executor.execute(request, middleware);
|
|
199
|
-
return
|
|
166
|
+
return responseStream(response);
|
|
200
167
|
}));
|
|
201
168
|
export const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
202
169
|
const http = yield* HttpClient.HttpClient;
|
|
203
170
|
const executeOnce = (request, middleware) => Effect.gen(function* () {
|
|
204
171
|
if (!middleware)
|
|
205
|
-
return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError
|
|
172
|
+
return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError));
|
|
206
173
|
const response = yield* middleware(request, (input) => http
|
|
207
174
|
.execute(input)
|
|
208
175
|
.pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })));
|
|
209
|
-
return yield* statusError(response
|
|
176
|
+
return yield* statusError(response);
|
|
210
177
|
});
|
|
211
178
|
return Service.of({
|
|
212
179
|
execute: executeOnce,
|
package/dist/route/framing.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ import type { AIError } from "../schema/index.js";
|
|
|
17
17
|
export interface Definition<Frame> {
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>;
|
|
20
|
+
/** Original wire representation when framing transforms the provider payload. */
|
|
21
|
+
readonly body?: (frame: Frame) => string | undefined;
|
|
20
22
|
}
|
|
21
23
|
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
|
22
24
|
export declare const sse: Definition<string>;
|
|
@@ -53,8 +53,13 @@ export const httpJson = (input) => ({
|
|
|
53
53
|
middleware: prepareInput.middleware,
|
|
54
54
|
};
|
|
55
55
|
}),
|
|
56
|
-
execute: (prepared, _request, runtime) => Effect.
|
|
57
|
-
|
|
56
|
+
execute: (prepared, _request, runtime) => Effect.gen(function* () {
|
|
57
|
+
const response = yield* runtime.http.execute(prepared.request, prepared.middleware);
|
|
58
|
+
return {
|
|
59
|
+
frames: prepared.framing.frame(RequestExecutor.responseStream(response)),
|
|
60
|
+
http: RequestExecutor.responseHttp(response),
|
|
61
|
+
body: prepared.framing.body,
|
|
62
|
+
};
|
|
58
63
|
}),
|
|
59
64
|
});
|
|
60
65
|
export const sseJson = {
|
|
@@ -3,12 +3,14 @@ import { Endpoint } from "../endpoint.js";
|
|
|
3
3
|
import { Auth } from "../auth.js";
|
|
4
4
|
import type { HttpMiddleware, Interface as RequestExecutorInterface } from "../executor.js";
|
|
5
5
|
import type { WebSocketChannelExecutor } from "./websocket-channel.js";
|
|
6
|
-
import type { AIError, LLMRequest } from "../../schema/index.js";
|
|
6
|
+
import type { AIError, HttpContext, LLMRequest } from "../../schema/index.js";
|
|
7
7
|
export interface TransportRuntime {
|
|
8
8
|
readonly http: RequestExecutorInterface;
|
|
9
9
|
}
|
|
10
10
|
export interface TransportExecution<Frame> {
|
|
11
11
|
readonly frames: Stream.Stream<Frame, AIError>;
|
|
12
|
+
readonly http?: HttpContext;
|
|
13
|
+
body?(frame: Frame): string | undefined;
|
|
12
14
|
/** Optional successful-consumption acknowledgement. HTTP leaves this absent. */
|
|
13
15
|
readonly complete?: Effect.Effect<void>;
|
|
14
16
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Effect, Scope, Stream } from "effect";
|
|
2
2
|
import type { Headers } from "effect/unstable/http";
|
|
3
|
-
import type { AIError } from "../../schema/index.js";
|
|
3
|
+
import type { AIError, HttpContext } from "../../schema/index.js";
|
|
4
4
|
export interface WebSocketChannelExecutor {
|
|
5
5
|
readonly execute: (exchange: WebSocketChannelExchange) => Effect.Effect<WebSocketChannelExecution, AIError, Scope.Scope>;
|
|
6
6
|
}
|
|
7
7
|
export interface WebSocketChannelExecution {
|
|
8
8
|
readonly frames: Stream.Stream<string, AIError>;
|
|
9
|
+
readonly http?: HttpContext;
|
|
9
10
|
/** Commits staged state after the decoded Route stream ends successfully. */
|
|
10
11
|
readonly complete: Effect.Effect<void>;
|
|
11
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
2
|
import { Headers } from "effect/unstable/http";
|
|
3
3
|
import { Socket } from "effect/unstable/socket";
|
|
4
|
-
import { AIError } from "../../schema/index.js";
|
|
4
|
+
import { AIError, type HttpContext } from "../../schema/index.js";
|
|
5
5
|
import type { Transport } from "./index.js";
|
|
6
6
|
import type { WebSocketChannelExecutor } from "./websocket-channel.js";
|
|
7
7
|
export interface WebSocketRequest {
|
|
@@ -9,6 +9,7 @@ export interface WebSocketRequest {
|
|
|
9
9
|
readonly headers: Headers.Headers;
|
|
10
10
|
}
|
|
11
11
|
export interface WebSocketConnection {
|
|
12
|
+
readonly http?: HttpContext;
|
|
12
13
|
readonly sendText: (message: string) => Effect.Effect<void, AIError>;
|
|
13
14
|
readonly messages: Stream.Stream<string | Uint8Array, AIError>;
|
|
14
15
|
readonly close: Effect.Effect<void, never>;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Cause, Effect, Queue, Stream } from "effect";
|
|
2
2
|
import { Headers } from "effect/unstable/http";
|
|
3
3
|
import { Socket } from "effect/unstable/socket";
|
|
4
|
-
import { AIError,
|
|
4
|
+
import { AIError, AIErrorReason, TransportError, } from "../../schema/index.js";
|
|
5
5
|
import * as HttpTransport from "./http.js";
|
|
6
6
|
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
7
|
-
const transportError = (
|
|
8
|
-
|
|
9
|
-
method,
|
|
10
|
-
reason: new TransportReason({
|
|
7
|
+
const transportError = (message, input) => new AIError({
|
|
8
|
+
reason: new TransportError({
|
|
11
9
|
message,
|
|
10
|
+
body: input.body,
|
|
11
|
+
cause: input.cause,
|
|
12
12
|
transport: "websocket",
|
|
13
13
|
operation: input.operation,
|
|
14
14
|
url: input.url,
|
|
@@ -19,18 +19,12 @@ const transportError = (method, message, input) => new AIError({
|
|
|
19
19
|
});
|
|
20
20
|
const annotateTransportError = (error, input) => error.reason._tag === "Transport"
|
|
21
21
|
? new AIError({
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
reason: new TransportReason({
|
|
22
|
+
reason: new TransportError({
|
|
23
|
+
...error.reason,
|
|
25
24
|
message: error.reason.message,
|
|
26
|
-
|
|
27
|
-
operation: error.reason.operation,
|
|
28
|
-
code: error.reason.code,
|
|
29
|
-
url: error.reason.url,
|
|
30
|
-
http: error.reason.http,
|
|
25
|
+
cause: error.reason.cause,
|
|
31
26
|
phase: input.phase,
|
|
32
27
|
delivery: input.delivery,
|
|
33
|
-
recovery: error.reason.recovery,
|
|
34
28
|
}),
|
|
35
29
|
})
|
|
36
30
|
: error;
|
|
@@ -52,7 +46,7 @@ const waitOpen = (ws, input) => {
|
|
|
52
46
|
if (ws.readyState === globalThis.WebSocket.OPEN)
|
|
53
47
|
return Effect.void;
|
|
54
48
|
if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) {
|
|
55
|
-
return Effect.fail(transportError(
|
|
49
|
+
return Effect.fail(transportError(`WebSocket closed before opening (state ${ws.readyState})`, {
|
|
56
50
|
url: input.url,
|
|
57
51
|
operation: "request",
|
|
58
52
|
code: "closed",
|
|
@@ -78,7 +72,8 @@ const waitOpen = (ws, input) => {
|
|
|
78
72
|
};
|
|
79
73
|
const onError = (event) => {
|
|
80
74
|
cleanup();
|
|
81
|
-
resume(Effect.fail(transportError(
|
|
75
|
+
resume(Effect.fail(transportError(`Failed to open WebSocket: ${eventMessage(event)}`, {
|
|
76
|
+
cause: "error" in event ? (event.error ?? event) : event,
|
|
82
77
|
url: input.url,
|
|
83
78
|
operation: "request",
|
|
84
79
|
phase: "connect",
|
|
@@ -87,7 +82,9 @@ const waitOpen = (ws, input) => {
|
|
|
87
82
|
};
|
|
88
83
|
const onClose = (event) => {
|
|
89
84
|
cleanup();
|
|
90
|
-
resume(Effect.fail(transportError(
|
|
85
|
+
resume(Effect.fail(transportError(`WebSocket closed before opening with code ${event.code}`, {
|
|
86
|
+
body: event.reason,
|
|
87
|
+
cause: event,
|
|
91
88
|
url: input.url,
|
|
92
89
|
operation: "request",
|
|
93
90
|
code: String(event.code),
|
|
@@ -114,7 +111,8 @@ export const toWebSocketUrl = (value) => Effect.try({
|
|
|
114
111
|
}
|
|
115
112
|
throw new Error(`Unsupported WebSocket URL protocol ${url.protocol}`);
|
|
116
113
|
},
|
|
117
|
-
catch: (error) => transportError(
|
|
114
|
+
catch: (error) => transportError(error instanceof Error ? error.message : "Invalid WebSocket URL", {
|
|
115
|
+
cause: error,
|
|
118
116
|
url: value,
|
|
119
117
|
operation: "request",
|
|
120
118
|
code: "invalid-url",
|
|
@@ -131,7 +129,8 @@ export const open = (input) => Effect.gen(function* () {
|
|
|
131
129
|
constructor(input.url, {
|
|
132
130
|
headers: input.headers,
|
|
133
131
|
}),
|
|
134
|
-
catch: (error) => transportError(
|
|
132
|
+
catch: (error) => transportError(error instanceof Error ? error.message : "Failed to construct WebSocket", {
|
|
133
|
+
cause: error,
|
|
135
134
|
url: input.url,
|
|
136
135
|
operation: "request",
|
|
137
136
|
phase: "connect",
|
|
@@ -147,7 +146,8 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
147
146
|
const rejectOversized = (message) => {
|
|
148
147
|
if (!oversized(message))
|
|
149
148
|
return false;
|
|
150
|
-
Queue.failCauseUnsafe(messages, Cause.fail(transportError("
|
|
149
|
+
Queue.failCauseUnsafe(messages, Cause.fail(transportError("WebSocket message exceeds the 16 MiB limit", {
|
|
150
|
+
body: typeof message === "string" ? message : new TextDecoder().decode(message),
|
|
151
151
|
url: input.url,
|
|
152
152
|
operation: "read",
|
|
153
153
|
code: "message-too-large",
|
|
@@ -162,7 +162,8 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
162
162
|
return;
|
|
163
163
|
if (Queue.offerUnsafe(messages, message))
|
|
164
164
|
return;
|
|
165
|
-
Queue.failCauseUnsafe(messages, Cause.fail(transportError("
|
|
165
|
+
Queue.failCauseUnsafe(messages, Cause.fail(transportError("WebSocket inbound queue overflow", {
|
|
166
|
+
body: typeof message === "string" ? message : new TextDecoder().decode(message),
|
|
166
167
|
url: input.url,
|
|
167
168
|
operation: "read",
|
|
168
169
|
code: "queue-overflow",
|
|
@@ -175,7 +176,8 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
175
176
|
const binary = binaryMessage(event.data);
|
|
176
177
|
if (binary)
|
|
177
178
|
return offer(binary);
|
|
178
|
-
Queue.failCauseUnsafe(messages, Cause.fail(transportError("
|
|
179
|
+
Queue.failCauseUnsafe(messages, Cause.fail(transportError("Unsupported WebSocket message payload", {
|
|
180
|
+
cause: event,
|
|
179
181
|
url: input.url,
|
|
180
182
|
operation: "read",
|
|
181
183
|
code: "message",
|
|
@@ -183,7 +185,8 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
183
185
|
})));
|
|
184
186
|
};
|
|
185
187
|
const onError = (event) => {
|
|
186
|
-
Queue.failCauseUnsafe(messages, Cause.fail(transportError(
|
|
188
|
+
Queue.failCauseUnsafe(messages, Cause.fail(transportError(`WebSocket error: ${eventMessage(event)}`, {
|
|
189
|
+
cause: "error" in event ? (event.error ?? event) : event,
|
|
187
190
|
url: input.url,
|
|
188
191
|
operation: "read",
|
|
189
192
|
code: "message",
|
|
@@ -191,7 +194,9 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
191
194
|
})));
|
|
192
195
|
};
|
|
193
196
|
const onClose = (event) => {
|
|
194
|
-
Queue.failCauseUnsafe(messages, Cause.fail(transportError(
|
|
197
|
+
Queue.failCauseUnsafe(messages, Cause.fail(transportError(`WebSocket closed with code ${event.code}`, {
|
|
198
|
+
body: event.reason,
|
|
199
|
+
cause: event,
|
|
195
200
|
url: input.url,
|
|
196
201
|
operation: "read",
|
|
197
202
|
code: String(event.code),
|
|
@@ -209,7 +214,7 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
209
214
|
return {
|
|
210
215
|
sendText: (message) => Effect.suspend(() => {
|
|
211
216
|
if (ws.readyState !== globalThis.WebSocket.OPEN)
|
|
212
|
-
return Effect.fail(transportError(
|
|
217
|
+
return Effect.fail(transportError(`WebSocket is not open (state ${ws.readyState})`, {
|
|
213
218
|
url: input.url,
|
|
214
219
|
operation: "write",
|
|
215
220
|
phase: "send",
|
|
@@ -217,7 +222,8 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
|
|
|
217
222
|
}));
|
|
218
223
|
return Effect.try({
|
|
219
224
|
try: () => ws.send(message),
|
|
220
|
-
catch: (error) => transportError(
|
|
225
|
+
catch: (error) => transportError(error instanceof Error ? error.message : "Failed to send WebSocket message", {
|
|
226
|
+
cause: error,
|
|
221
227
|
url: input.url,
|
|
222
228
|
operation: "write",
|
|
223
229
|
phase: "send",
|
|
@@ -246,17 +252,51 @@ export const makeDirect = (connector) => ({
|
|
|
246
252
|
.open(exchange.connect)
|
|
247
253
|
.pipe(Effect.mapError((error) => annotateTransportError(error, { phase: "connect", delivery: "not-sent" }))), (connection) => connection.close);
|
|
248
254
|
const create = yield* exchange.driver.create(undefined);
|
|
249
|
-
yield* connection.sendText(create.message)
|
|
255
|
+
yield* connection.sendText(create.message).pipe(Effect.mapError((error) => new AIError({
|
|
256
|
+
reason: AIErrorReason.make({
|
|
257
|
+
...error.reason,
|
|
258
|
+
message: error.reason.message,
|
|
259
|
+
cause: error.reason.cause,
|
|
260
|
+
http: error.reason.http ?? connection.http,
|
|
261
|
+
}),
|
|
262
|
+
})));
|
|
250
263
|
const decoder = new TextDecoder();
|
|
251
264
|
let observed = false;
|
|
252
265
|
return {
|
|
266
|
+
http: connection.http,
|
|
253
267
|
frames: connection.messages.pipe(Stream.map((message) => {
|
|
254
268
|
observed = true;
|
|
255
269
|
return messageText(message, decoder);
|
|
256
270
|
}), Stream.mapError((error) => annotateTransportError(error, {
|
|
257
271
|
phase: error.reason._tag === "Transport" && error.reason.phase === "close" ? "close" : "receive",
|
|
258
272
|
delivery: observed ? "accepted" : "ambiguous",
|
|
259
|
-
})), Stream.mapEffect((frame) => exchange.driver.observe(create, frame)
|
|
273
|
+
})), Stream.mapEffect((frame) => exchange.driver.observe(create, frame).pipe(Effect.mapError((error) => new AIError({
|
|
274
|
+
reason: AIErrorReason.make({
|
|
275
|
+
...error.reason,
|
|
276
|
+
message: error.reason.message,
|
|
277
|
+
cause: error.reason.cause,
|
|
278
|
+
body: frame,
|
|
279
|
+
}),
|
|
280
|
+
})), Effect.map((observation) => "error" in observation
|
|
281
|
+
? {
|
|
282
|
+
...observation,
|
|
283
|
+
error: new AIError({
|
|
284
|
+
reason: AIErrorReason.make({
|
|
285
|
+
...observation.error.reason,
|
|
286
|
+
message: observation.error.reason.message,
|
|
287
|
+
cause: observation.error.reason.cause,
|
|
288
|
+
body: frame,
|
|
289
|
+
}),
|
|
290
|
+
}),
|
|
291
|
+
}
|
|
292
|
+
: observation))), Stream.takeUntil(observationTerminal), Stream.mapEffect(observationFrame), Stream.mapError((error) => new AIError({
|
|
293
|
+
reason: AIErrorReason.make({
|
|
294
|
+
...error.reason,
|
|
295
|
+
message: error.reason.message,
|
|
296
|
+
cause: error.reason.cause,
|
|
297
|
+
http: error.reason.http ?? connection.http,
|
|
298
|
+
}),
|
|
299
|
+
}))),
|
|
260
300
|
complete: Effect.void,
|
|
261
301
|
};
|
|
262
302
|
}),
|
|
@@ -283,7 +323,7 @@ export const json = (input) => ({
|
|
|
283
323
|
execute: (prepared, request, _runtime, options) => {
|
|
284
324
|
const webSocket = options?.webSocket;
|
|
285
325
|
if (!webSocket) {
|
|
286
|
-
return Effect.fail(transportError("
|
|
326
|
+
return Effect.fail(transportError("WebSocket JSON transport requires StreamOptions.webSocket", {
|
|
287
327
|
url: prepared.url,
|
|
288
328
|
operation: "request",
|
|
289
329
|
code: "unavailable",
|
|
@@ -298,7 +338,7 @@ export const json = (input) => ({
|
|
|
298
338
|
const exchange = {
|
|
299
339
|
id: request.id ?? "request",
|
|
300
340
|
connect: { url: prepared.url, headers: prepared.headers },
|
|
301
|
-
fallback: () => Stream.fail(transportError("
|
|
341
|
+
fallback: () => Stream.fail(transportError("WebSocket JSON transport does not provide HTTP fallback", {
|
|
302
342
|
url: prepared.url,
|
|
303
343
|
operation: "request",
|
|
304
344
|
code: "websocket",
|