@effect/ai-openai 4.0.0-beta.7 → 4.0.0-beta.70

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