@effect/ai-openai 4.0.0-beta.6 → 4.0.0-beta.60
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/Generated.d.ts +66011 -38686
- package/dist/Generated.d.ts.map +1 -1
- package/dist/Generated.js +1 -1
- package/dist/Generated.js.map +1 -1
- package/dist/OpenAiClient.d.ts +63 -17
- package/dist/OpenAiClient.d.ts.map +1 -1
- package/dist/OpenAiClient.js +210 -33
- package/dist/OpenAiClient.js.map +1 -1
- package/dist/OpenAiClientGenerated.d.ts +91 -0
- package/dist/OpenAiClientGenerated.d.ts.map +1 -0
- package/dist/OpenAiClientGenerated.js +84 -0
- package/dist/OpenAiClientGenerated.js.map +1 -0
- package/dist/OpenAiConfig.d.ts +2 -2
- package/dist/OpenAiConfig.d.ts.map +1 -1
- package/dist/OpenAiConfig.js +3 -3
- package/dist/OpenAiConfig.js.map +1 -1
- package/dist/OpenAiEmbeddingModel.d.ts +85 -0
- package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
- package/dist/OpenAiEmbeddingModel.js +119 -0
- package/dist/OpenAiEmbeddingModel.js.map +1 -0
- package/dist/OpenAiError.d.ts +22 -32
- package/dist/OpenAiError.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.d.ts +43 -49
- package/dist/OpenAiLanguageModel.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.js +296 -152
- package/dist/OpenAiLanguageModel.js.map +1 -1
- package/dist/OpenAiSchema.d.ts +1920 -0
- package/dist/OpenAiSchema.d.ts.map +1 -0
- package/dist/OpenAiSchema.js +536 -0
- package/dist/OpenAiSchema.js.map +1 -0
- package/dist/OpenAiTool.d.ts +8 -7
- package/dist/OpenAiTool.d.ts.map +1 -1
- package/dist/OpenAiTool.js +2 -1
- package/dist/OpenAiTool.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/errors.js +4 -4
- package/dist/internal/errors.js.map +1 -1
- package/package.json +3 -3
- package/src/Generated.ts +7416 -4257
- package/src/OpenAiClient.ts +377 -81
- package/src/OpenAiClientGenerated.ts +202 -0
- package/src/OpenAiConfig.ts +3 -3
- package/src/OpenAiEmbeddingModel.ts +203 -0
- package/src/OpenAiError.ts +24 -32
- package/src/OpenAiLanguageModel.ts +420 -144
- package/src/OpenAiSchema.ts +875 -0
- package/src/OpenAiTool.ts +2 -1
- package/src/index.ts +21 -0
- package/src/internal/errors.ts +6 -4
package/src/OpenAiClient.ts
CHANGED
|
@@ -8,23 +8,31 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as Array from "effect/Array"
|
|
10
10
|
import type * as Config from "effect/Config"
|
|
11
|
+
import * as Context from "effect/Context"
|
|
11
12
|
import * as Effect from "effect/Effect"
|
|
12
13
|
import { identity } from "effect/Function"
|
|
14
|
+
import * as Function from "effect/Function"
|
|
13
15
|
import * as Layer from "effect/Layer"
|
|
14
16
|
import * as Predicate from "effect/Predicate"
|
|
17
|
+
import * as Queue from "effect/Queue"
|
|
18
|
+
import * as RcRef from "effect/RcRef"
|
|
15
19
|
import * as Redacted from "effect/Redacted"
|
|
16
|
-
import * as
|
|
20
|
+
import * as Schema from "effect/Schema"
|
|
21
|
+
import * as Scope from "effect/Scope"
|
|
22
|
+
import * as Semaphore from "effect/Semaphore"
|
|
17
23
|
import * as Stream from "effect/Stream"
|
|
18
|
-
import
|
|
24
|
+
import * as AiError from "effect/unstable/ai/AiError"
|
|
25
|
+
import * as ResponseIdTracker from "effect/unstable/ai/ResponseIdTracker"
|
|
19
26
|
import * as Sse from "effect/unstable/encoding/Sse"
|
|
20
27
|
import * as Headers from "effect/unstable/http/Headers"
|
|
21
28
|
import * as HttpBody from "effect/unstable/http/HttpBody"
|
|
22
29
|
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
23
30
|
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
24
|
-
import
|
|
25
|
-
import * as
|
|
31
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
32
|
+
import * as Socket from "effect/unstable/socket/Socket"
|
|
26
33
|
import * as Errors from "./internal/errors.ts"
|
|
27
34
|
import { OpenAiConfig } from "./OpenAiConfig.ts"
|
|
35
|
+
import * as OpenAiSchema from "./OpenAiSchema.ts"
|
|
28
36
|
|
|
29
37
|
// =============================================================================
|
|
30
38
|
// Service Interface
|
|
@@ -38,17 +46,17 @@ import { OpenAiConfig } from "./OpenAiConfig.ts"
|
|
|
38
46
|
*/
|
|
39
47
|
export interface Service {
|
|
40
48
|
/**
|
|
41
|
-
* The
|
|
49
|
+
* The transformed HTTP client used by this service.
|
|
42
50
|
*/
|
|
43
|
-
readonly client:
|
|
51
|
+
readonly client: HttpClient.HttpClient
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
54
|
* Create a response using the OpenAI responses endpoint.
|
|
47
55
|
*/
|
|
48
56
|
readonly createResponse: (
|
|
49
|
-
options: typeof
|
|
57
|
+
options: typeof OpenAiSchema.CreateResponse.Encoded
|
|
50
58
|
) => Effect.Effect<
|
|
51
|
-
[body: typeof
|
|
59
|
+
readonly [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
52
60
|
AiError.AiError
|
|
53
61
|
>
|
|
54
62
|
|
|
@@ -56,11 +64,11 @@ export interface Service {
|
|
|
56
64
|
* Create a streaming response using the OpenAI responses endpoint.
|
|
57
65
|
*/
|
|
58
66
|
readonly createResponseStream: (
|
|
59
|
-
options: Omit<typeof
|
|
67
|
+
options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
|
|
60
68
|
) => Effect.Effect<
|
|
61
|
-
[
|
|
69
|
+
readonly [
|
|
62
70
|
response: HttpClientResponse.HttpClientResponse,
|
|
63
|
-
stream: Stream.Stream<typeof
|
|
71
|
+
stream: Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
|
|
64
72
|
],
|
|
65
73
|
AiError.AiError
|
|
66
74
|
>
|
|
@@ -69,8 +77,8 @@ export interface Service {
|
|
|
69
77
|
* Create embeddings using the OpenAI embeddings endpoint.
|
|
70
78
|
*/
|
|
71
79
|
readonly createEmbedding: (
|
|
72
|
-
options: typeof
|
|
73
|
-
) => Effect.Effect<typeof
|
|
80
|
+
options: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
|
|
81
|
+
) => Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError>
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
// =============================================================================
|
|
@@ -83,7 +91,7 @@ export interface Service {
|
|
|
83
91
|
* @since 1.0.0
|
|
84
92
|
* @category service
|
|
85
93
|
*/
|
|
86
|
-
export class OpenAiClient extends
|
|
94
|
+
export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
|
|
87
95
|
"@effect/ai-openai/OpenAiClient"
|
|
88
96
|
) {}
|
|
89
97
|
|
|
@@ -142,70 +150,83 @@ const RedactedOpenAiHeaders = {
|
|
|
142
150
|
* @category constructors
|
|
143
151
|
*/
|
|
144
152
|
export const make = Effect.fnUntraced(
|
|
145
|
-
function*(
|
|
153
|
+
function*(
|
|
154
|
+
options: Options
|
|
155
|
+
): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
|
|
146
156
|
const baseClient = yield* HttpClient.HttpClient
|
|
157
|
+
const apiUrl = options.apiUrl ?? "https://api.openai.com/v1"
|
|
147
158
|
|
|
148
159
|
const httpClient = baseClient.pipe(
|
|
149
|
-
HttpClient.mapRequest((
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Predicate.isNotUndefined(options.transformClient)
|
|
160
|
+
HttpClient.mapRequest(Function.flow(
|
|
161
|
+
HttpClientRequest.prependUrl(apiUrl),
|
|
162
|
+
options.apiKey
|
|
163
|
+
? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
|
|
164
|
+
: identity,
|
|
165
|
+
options.organizationId
|
|
166
|
+
? HttpClientRequest.setHeader(
|
|
167
|
+
RedactedOpenAiHeaders.OpenAiOrganization,
|
|
168
|
+
Redacted.value(options.organizationId)
|
|
169
|
+
)
|
|
170
|
+
: identity,
|
|
171
|
+
options.projectId
|
|
172
|
+
? HttpClientRequest.setHeader(
|
|
173
|
+
RedactedOpenAiHeaders.OpenAiProject,
|
|
174
|
+
Redacted.value(options.projectId)
|
|
175
|
+
)
|
|
176
|
+
: identity,
|
|
177
|
+
HttpClientRequest.acceptJson
|
|
178
|
+
)),
|
|
179
|
+
HttpClient.filterStatusOk,
|
|
180
|
+
options.transformClient
|
|
171
181
|
? options.transformClient
|
|
172
182
|
: identity
|
|
173
183
|
)
|
|
174
184
|
|
|
175
|
-
const
|
|
185
|
+
const resolveHttpClient = Effect.map(
|
|
186
|
+
OpenAiConfig.getOrUndefined,
|
|
187
|
+
(config) =>
|
|
188
|
+
Predicate.isNotUndefined(config?.transformClient)
|
|
189
|
+
? config.transformClient(httpClient)
|
|
190
|
+
: httpClient
|
|
191
|
+
)
|
|
176
192
|
|
|
177
|
-
const
|
|
178
|
-
transformClient: Effect.fnUntraced(function*(client) {
|
|
179
|
-
const config = yield* OpenAiConfig.getOrUndefined
|
|
180
|
-
if (Predicate.isNotUndefined(config?.transformClient)) {
|
|
181
|
-
return config.transformClient(client)
|
|
182
|
-
}
|
|
183
|
-
return client
|
|
184
|
-
})
|
|
185
|
-
})
|
|
193
|
+
const decodeResponse = HttpClientResponse.schemaBodyJson(OpenAiSchema.Response)
|
|
186
194
|
|
|
187
195
|
const createResponse = (
|
|
188
|
-
payload: typeof
|
|
196
|
+
payload: typeof OpenAiSchema.CreateResponse.Encoded
|
|
189
197
|
): Effect.Effect<
|
|
190
|
-
[body: typeof
|
|
198
|
+
[body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
191
199
|
AiError.AiError
|
|
192
200
|
> =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
Effect.flatMap(resolveHttpClient, (client) =>
|
|
202
|
+
client.execute(
|
|
203
|
+
HttpClientRequest.post("/responses", {
|
|
204
|
+
body: HttpBody.jsonUnsafe(payload)
|
|
205
|
+
})
|
|
206
|
+
).pipe(
|
|
207
|
+
Effect.flatMap((response) =>
|
|
208
|
+
decodeResponse(response).pipe(
|
|
209
|
+
Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [
|
|
210
|
+
body,
|
|
211
|
+
response
|
|
212
|
+
])
|
|
213
|
+
)
|
|
214
|
+
),
|
|
215
|
+
Effect.catchTags({
|
|
216
|
+
HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"),
|
|
217
|
+
SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse"))
|
|
218
|
+
})
|
|
219
|
+
))
|
|
199
220
|
|
|
200
221
|
const buildResponseStream = (
|
|
201
222
|
response: HttpClientResponse.HttpClientResponse
|
|
202
223
|
): [
|
|
203
224
|
HttpClientResponse.HttpClientResponse,
|
|
204
|
-
Stream.Stream<typeof
|
|
225
|
+
Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
|
|
205
226
|
] => {
|
|
206
227
|
const stream = response.stream.pipe(
|
|
207
228
|
Stream.decodeText(),
|
|
208
|
-
Stream.pipeThroughChannel(Sse.decodeDataSchema(
|
|
229
|
+
Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)),
|
|
209
230
|
Stream.takeUntil((event) =>
|
|
210
231
|
event.data.type === "response.completed" ||
|
|
211
232
|
event.data.type === "response.incomplete"
|
|
@@ -217,35 +238,48 @@ export const make = Effect.fnUntraced(
|
|
|
217
238
|
HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")),
|
|
218
239
|
SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createResponseStream"))
|
|
219
240
|
})
|
|
220
|
-
)
|
|
241
|
+
)
|
|
221
242
|
return [response, stream]
|
|
222
243
|
}
|
|
223
244
|
|
|
224
245
|
const createResponseStream: Service["createResponseStream"] = (payload) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
246
|
+
Effect.contextWith((services) => {
|
|
247
|
+
const socket = Context.getOrUndefined(services, OpenAiSocket)
|
|
248
|
+
if (socket) return socket.createResponseStream(payload)
|
|
249
|
+
return Effect.flatMap(resolveHttpClient, (client) =>
|
|
250
|
+
client.execute(
|
|
251
|
+
HttpClientRequest.post("/responses", {
|
|
252
|
+
body: HttpBody.jsonUnsafe({ ...payload, stream: true })
|
|
253
|
+
})
|
|
254
|
+
).pipe(
|
|
255
|
+
Effect.map(buildResponseStream),
|
|
256
|
+
Effect.catchTag(
|
|
257
|
+
"HttpClientError",
|
|
258
|
+
(error) => Errors.mapHttpClientError(error, "createResponseStream")
|
|
259
|
+
)
|
|
260
|
+
))
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const decodeEmbedding = HttpClientResponse.schemaBodyJson(OpenAiSchema.CreateEmbeddingResponse)
|
|
236
264
|
|
|
237
265
|
const createEmbedding = (
|
|
238
|
-
payload: typeof
|
|
239
|
-
): Effect.Effect<typeof
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
payload: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
|
|
267
|
+
): Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError> =>
|
|
268
|
+
Effect.flatMap(resolveHttpClient, (client) =>
|
|
269
|
+
client.execute(
|
|
270
|
+
HttpClientRequest.post("/embeddings", {
|
|
271
|
+
body: HttpBody.jsonUnsafe(payload)
|
|
272
|
+
})
|
|
273
|
+
).pipe(
|
|
274
|
+
Effect.flatMap(decodeEmbedding),
|
|
275
|
+
Effect.catchTags({
|
|
276
|
+
HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"),
|
|
277
|
+
SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding"))
|
|
278
|
+
})
|
|
279
|
+
))
|
|
246
280
|
|
|
247
281
|
return OpenAiClient.of({
|
|
248
|
-
client,
|
|
282
|
+
client: httpClient,
|
|
249
283
|
createResponse,
|
|
250
284
|
createResponseStream,
|
|
251
285
|
createEmbedding
|
|
@@ -281,7 +315,7 @@ export const layerConfig = (options?: {
|
|
|
281
315
|
/**
|
|
282
316
|
* The config value to load for the API key.
|
|
283
317
|
*/
|
|
284
|
-
readonly apiKey?: Config.Config<Redacted.Redacted<string
|
|
318
|
+
readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
285
319
|
|
|
286
320
|
/**
|
|
287
321
|
* The config value to load for the API URL.
|
|
@@ -291,12 +325,12 @@ export const layerConfig = (options?: {
|
|
|
291
325
|
/**
|
|
292
326
|
* The config value to load for the organization ID.
|
|
293
327
|
*/
|
|
294
|
-
readonly organizationId?: Config.Config<Redacted.Redacted<string
|
|
328
|
+
readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
295
329
|
|
|
296
330
|
/**
|
|
297
331
|
* The config value to load for the project ID.
|
|
298
332
|
*/
|
|
299
|
-
readonly projectId?: Config.Config<Redacted.Redacted<string
|
|
333
|
+
readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
300
334
|
|
|
301
335
|
/**
|
|
302
336
|
* Optional transformer for the HTTP client.
|
|
@@ -327,3 +361,265 @@ export const layerConfig = (options?: {
|
|
|
327
361
|
})
|
|
328
362
|
})
|
|
329
363
|
)
|
|
364
|
+
|
|
365
|
+
// =============================================================================
|
|
366
|
+
// Websocket mode
|
|
367
|
+
// =============================================================================
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @since 1.0.0
|
|
371
|
+
* @category Events
|
|
372
|
+
*/
|
|
373
|
+
export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @since 1.0.0
|
|
377
|
+
* @category Websocket mode
|
|
378
|
+
*/
|
|
379
|
+
export class OpenAiSocket extends Context.Service<OpenAiSocket, {
|
|
380
|
+
/**
|
|
381
|
+
* Create a streaming response using the OpenAI responses endpoint.
|
|
382
|
+
*/
|
|
383
|
+
readonly createResponseStream: (
|
|
384
|
+
options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
|
|
385
|
+
) => Effect.Effect<
|
|
386
|
+
readonly [
|
|
387
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
388
|
+
stream: Stream.Stream<ResponseStreamEvent, AiError.AiError>
|
|
389
|
+
],
|
|
390
|
+
AiError.AiError
|
|
391
|
+
>
|
|
392
|
+
}>()("@effect/ai-openai/OpenAiClient/OpenAiSocket") {}
|
|
393
|
+
|
|
394
|
+
const makeSocket = Effect.gen(function*() {
|
|
395
|
+
const client = yield* OpenAiClient
|
|
396
|
+
const tracker = yield* ResponseIdTracker.make
|
|
397
|
+
const socketScope = yield* Effect.scope
|
|
398
|
+
const makeRequest = Effect.flatMap(
|
|
399
|
+
OpenAiConfig.getOrUndefined,
|
|
400
|
+
(config) => {
|
|
401
|
+
const httpClient = Predicate.isNotUndefined(config?.transformClient)
|
|
402
|
+
? config.transformClient(client.client)
|
|
403
|
+
: client.client
|
|
404
|
+
return Effect.orDie(httpClient.preprocess(HttpClientRequest.post("/responses")))
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
const makeWebSocket = yield* Socket.WebSocketConstructor
|
|
408
|
+
|
|
409
|
+
const decoder = new TextDecoder()
|
|
410
|
+
|
|
411
|
+
const queueRef: RcRef.RcRef<
|
|
412
|
+
{
|
|
413
|
+
readonly send: (message: typeof OpenAiSchema.CreateResponse.Encoded) => Effect.Effect<void, AiError.AiError>
|
|
414
|
+
readonly incoming: Queue.Dequeue<ResponseStreamEvent, AiError.AiError>
|
|
415
|
+
}
|
|
416
|
+
> = yield* RcRef.make({
|
|
417
|
+
idleTimeToLive: 60_000,
|
|
418
|
+
acquire: Effect.gen(function*() {
|
|
419
|
+
const scope = yield* Effect.scope
|
|
420
|
+
const request = yield* makeRequest
|
|
421
|
+
const socket = yield* Socket.makeWebSocket(request.url.replace(/^http/, "ws")).pipe(
|
|
422
|
+
Effect.provideService(Socket.WebSocketConstructor, (url) =>
|
|
423
|
+
makeWebSocket(url, {
|
|
424
|
+
headers: request.headers
|
|
425
|
+
} as any))
|
|
426
|
+
)
|
|
427
|
+
const write = yield* socket.writer
|
|
428
|
+
|
|
429
|
+
yield* Scope.addFinalizerExit(scope, () => {
|
|
430
|
+
tracker.clearUnsafe()
|
|
431
|
+
return Effect.void
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const incoming = yield* Queue.unbounded<ResponseStreamEvent, AiError.AiError>()
|
|
435
|
+
const send = (message: typeof OpenAiSchema.CreateResponse.Encoded) =>
|
|
436
|
+
write(JSON.stringify({
|
|
437
|
+
type: "response.create",
|
|
438
|
+
...message
|
|
439
|
+
})).pipe(
|
|
440
|
+
Effect.mapError((_error) =>
|
|
441
|
+
AiError.make({
|
|
442
|
+
module: "OpenAiClient",
|
|
443
|
+
method: "createResponseStream",
|
|
444
|
+
reason: new AiError.NetworkError({
|
|
445
|
+
reason: "TransportError",
|
|
446
|
+
request: {
|
|
447
|
+
method: "POST",
|
|
448
|
+
url: request.url,
|
|
449
|
+
urlParams: [],
|
|
450
|
+
hash: undefined,
|
|
451
|
+
headers: request.headers
|
|
452
|
+
},
|
|
453
|
+
description: "Failed to send message over WebSocket"
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
yield* socket.runRaw((msg) => {
|
|
460
|
+
const text = typeof msg === "string" ? msg : decoder.decode(msg)
|
|
461
|
+
try {
|
|
462
|
+
const event = decodeEvent(text)
|
|
463
|
+
if (event.type === "error" && "status" in event) {
|
|
464
|
+
const status = Number(event.status)
|
|
465
|
+
const error = "error" in event ? event.error : event
|
|
466
|
+
const json = JSON.stringify(error)
|
|
467
|
+
return Effect.fail(
|
|
468
|
+
AiError.make({
|
|
469
|
+
module: "OpenAiClient",
|
|
470
|
+
method: "createResponseStream",
|
|
471
|
+
reason: AiError.reasonFromHttpStatus({
|
|
472
|
+
description: json,
|
|
473
|
+
status: isNaN(status) ? 500 : status,
|
|
474
|
+
metadata: error as any,
|
|
475
|
+
http: {
|
|
476
|
+
body: json,
|
|
477
|
+
request: {
|
|
478
|
+
method: "POST",
|
|
479
|
+
url: request.url,
|
|
480
|
+
urlParams: [],
|
|
481
|
+
hash: undefined,
|
|
482
|
+
headers: request.headers
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
Queue.offerUnsafe(incoming, event)
|
|
490
|
+
} catch {}
|
|
491
|
+
}).pipe(
|
|
492
|
+
Effect.catchTag("SocketError", (error) =>
|
|
493
|
+
AiError.make({
|
|
494
|
+
module: "OpenAiClient",
|
|
495
|
+
method: "createResponseStream",
|
|
496
|
+
reason: new AiError.NetworkError({
|
|
497
|
+
reason: "TransportError",
|
|
498
|
+
request: {
|
|
499
|
+
method: "POST",
|
|
500
|
+
url: request.url,
|
|
501
|
+
urlParams: [],
|
|
502
|
+
hash: undefined,
|
|
503
|
+
headers: request.headers
|
|
504
|
+
},
|
|
505
|
+
description: error.message
|
|
506
|
+
})
|
|
507
|
+
}).asEffect()),
|
|
508
|
+
Effect.catchCause((cause) => Queue.failCause(incoming, cause)),
|
|
509
|
+
Effect.ensuring(Effect.forkIn(RcRef.invalidate(queueRef), socketScope, {
|
|
510
|
+
startImmediately: true
|
|
511
|
+
})),
|
|
512
|
+
Effect.forkScoped({ startImmediately: true })
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return { send, incoming } as const
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
// Prime the websocket
|
|
520
|
+
yield* Effect.scoped(RcRef.get(queueRef))
|
|
521
|
+
|
|
522
|
+
// Websocket mode only allows one request at a time
|
|
523
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
524
|
+
const request = yield* makeRequest
|
|
525
|
+
|
|
526
|
+
return OpenAiSocket.context({
|
|
527
|
+
createResponseStream(options) {
|
|
528
|
+
const stream = Stream.unwrap(Effect.gen(function*() {
|
|
529
|
+
const scope = yield* Effect.scope
|
|
530
|
+
yield* Effect.acquireRelease(
|
|
531
|
+
semaphore.take(1),
|
|
532
|
+
() => semaphore.release(1),
|
|
533
|
+
{ interruptible: true }
|
|
534
|
+
)
|
|
535
|
+
const { send, incoming } = yield* RcRef.get(queueRef)
|
|
536
|
+
let done = false
|
|
537
|
+
|
|
538
|
+
yield* Scope.addFinalizerExit(
|
|
539
|
+
scope,
|
|
540
|
+
() => done ? Effect.void : RcRef.invalidate(queueRef)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
yield* send(options).pipe(
|
|
544
|
+
Effect.forkScoped({ startImmediately: true })
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return Stream.fromQueue(incoming).pipe(
|
|
548
|
+
Stream.takeUntil((e) => {
|
|
549
|
+
done = e.type === "response.completed" || e.type === "response.incomplete"
|
|
550
|
+
return done
|
|
551
|
+
})
|
|
552
|
+
)
|
|
553
|
+
}))
|
|
554
|
+
|
|
555
|
+
return Effect.succeed([
|
|
556
|
+
HttpClientResponse.fromWeb(request, new Response()),
|
|
557
|
+
stream
|
|
558
|
+
])
|
|
559
|
+
}
|
|
560
|
+
}).pipe(
|
|
561
|
+
Context.add(ResponseIdTracker.ResponseIdTracker, tracker)
|
|
562
|
+
)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const ErrorEvent = Schema.Struct({
|
|
566
|
+
type: Schema.Literal("error"),
|
|
567
|
+
status: Schema.Number.pipe(
|
|
568
|
+
Schema.withDecodingDefault(Effect.succeed(500))
|
|
569
|
+
),
|
|
570
|
+
error: Schema.Struct({
|
|
571
|
+
type: Schema.String,
|
|
572
|
+
message: Schema.String
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const AllEvents = Schema.Union([ErrorEvent, OpenAiSchema.ResponseStreamEvent])
|
|
577
|
+
const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents))
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Uses OpenAI's websocket mode for all responses within the provided effect.
|
|
581
|
+
*
|
|
582
|
+
* Note: This only works with the following WebSocket constructor layers:
|
|
583
|
+
*
|
|
584
|
+
* - `NodeSocket.layerWebSocketConstructorWS`
|
|
585
|
+
* - `BunSocket.layerWebSocketConstructor`
|
|
586
|
+
*
|
|
587
|
+
* This is because it needs to use non-standard options for setting the
|
|
588
|
+
* Authorization header.
|
|
589
|
+
*
|
|
590
|
+
* @since 1.0.0
|
|
591
|
+
* @category Websocket mode
|
|
592
|
+
*/
|
|
593
|
+
export const withWebSocketMode = <A, E, R>(
|
|
594
|
+
effect: Effect.Effect<A, E, R>
|
|
595
|
+
): Effect.Effect<
|
|
596
|
+
A,
|
|
597
|
+
E,
|
|
598
|
+
Exclude<R, OpenAiSocket | ResponseIdTracker.ResponseIdTracker> | OpenAiClient | Socket.WebSocketConstructor
|
|
599
|
+
> =>
|
|
600
|
+
Effect.scopedWith((scope) =>
|
|
601
|
+
Effect.flatMap(
|
|
602
|
+
Scope.provide(makeSocket, scope),
|
|
603
|
+
(services) => Effect.provideContext(effect, services)
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Uses OpenAI's websocket mode for all responses that use the Layer.
|
|
609
|
+
*
|
|
610
|
+
* Note: This only works with the following WebSocket constructor layers:
|
|
611
|
+
*
|
|
612
|
+
* - `NodeSocket.layerWebSocketConstructorWS`
|
|
613
|
+
* - `BunSocket.layerWebSocketConstructor`
|
|
614
|
+
*
|
|
615
|
+
* This is because it needs to use non-standard options for setting the
|
|
616
|
+
* Authorization header.
|
|
617
|
+
*
|
|
618
|
+
* @since 1.0.0
|
|
619
|
+
* @category Websocket mode
|
|
620
|
+
*/
|
|
621
|
+
export const layerWebSocketMode: Layer.Layer<
|
|
622
|
+
OpenAiSocket | ResponseIdTracker.ResponseIdTracker,
|
|
623
|
+
never,
|
|
624
|
+
OpenAiClient | Socket.WebSocketConstructor
|
|
625
|
+
> = Layer.effectContext(makeSocket)
|