@effect/ai-openai 4.0.0-beta.8 → 4.0.0-beta.81

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 +66675 -37672
  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 +170 -29
  6. package/dist/OpenAiClient.d.ts.map +1 -1
  7. package/dist/OpenAiClient.js +321 -46
  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 +88 -10
  14. package/dist/OpenAiConfig.d.ts.map +1 -1
  15. package/dist/OpenAiConfig.js +42 -7
  16. package/dist/OpenAiConfig.js.map +1 -1
  17. package/dist/OpenAiEmbeddingModel.d.ts +188 -0
  18. package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
  19. package/dist/OpenAiEmbeddingModel.js +194 -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 +357 -63
  25. package/dist/OpenAiLanguageModel.d.ts.map +1 -1
  26. package/dist/OpenAiLanguageModel.js +390 -168
  27. package/dist/OpenAiLanguageModel.js.map +1 -1
  28. package/dist/OpenAiSchema.d.ts +2325 -0
  29. package/dist/OpenAiSchema.d.ts.map +1 -0
  30. package/dist/OpenAiSchema.js +811 -0
  31. package/dist/OpenAiSchema.js.map +1 -0
  32. package/dist/OpenAiTelemetry.d.ts +63 -22
  33. package/dist/OpenAiTelemetry.d.ts.map +1 -1
  34. package/dist/OpenAiTelemetry.js +20 -10
  35. package/dist/OpenAiTelemetry.js.map +1 -1
  36. package/dist/OpenAiTool.d.ts +148 -62
  37. package/dist/OpenAiTool.d.ts.map +1 -1
  38. package/dist/OpenAiTool.js +125 -39
  39. package/dist/OpenAiTool.js.map +1 -1
  40. package/dist/index.d.ts +19 -33
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +19 -33
  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 +9717 -4887
  48. package/src/OpenAiClient.ts +499 -98
  49. package/src/OpenAiClientGenerated.ts +202 -0
  50. package/src/OpenAiConfig.ts +89 -11
  51. package/src/OpenAiEmbeddingModel.ts +332 -0
  52. package/src/OpenAiError.ts +170 -35
  53. package/src/OpenAiLanguageModel.ts +776 -169
  54. package/src/OpenAiSchema.ts +1286 -0
  55. package/src/OpenAiTelemetry.ts +69 -28
  56. package/src/OpenAiTool.ts +126 -40
  57. package/src/index.ts +22 -33
  58. package/src/internal/errors.ts +6 -4
@@ -1,54 +1,68 @@
1
1
  /**
2
- * OpenAI Client module for interacting with OpenAI's API.
2
+ * The `OpenAiClient` module defines the low-level Effect service used by the
3
+ * OpenAI integration for Responses API and embedding requests. It builds a
4
+ * configured HTTP client with authentication and OpenAI organization or project
5
+ * headers, exposes helpers for non-streaming responses, SSE response streams,
6
+ * WebSocket response streams, and embeddings, and maps transport or decoding
7
+ * failures into `AiError`.
3
8
  *
4
- * Provides a type-safe, Effect-based client for OpenAI operations including
5
- * completions, embeddings, and streaming responses.
6
- *
7
- * @since 1.0.0
9
+ * @since 4.0.0
8
10
  */
9
11
  import * as Array from "effect/Array"
10
12
  import type * as Config from "effect/Config"
13
+ import * as Context from "effect/Context"
11
14
  import * as Effect from "effect/Effect"
12
15
  import { identity } from "effect/Function"
16
+ import * as Function from "effect/Function"
13
17
  import * as Layer from "effect/Layer"
14
18
  import * as Predicate from "effect/Predicate"
19
+ import * as Queue from "effect/Queue"
20
+ import * as RcRef from "effect/RcRef"
15
21
  import * as Redacted from "effect/Redacted"
16
- import * as ServiceMap from "effect/ServiceMap"
22
+ import * as Schema from "effect/Schema"
23
+ import * as Scope from "effect/Scope"
24
+ import * as Semaphore from "effect/Semaphore"
17
25
  import * as Stream from "effect/Stream"
18
- import type * as AiError from "effect/unstable/ai/AiError"
26
+ import * as AiError from "effect/unstable/ai/AiError"
27
+ import * as ResponseIdTracker from "effect/unstable/ai/ResponseIdTracker"
19
28
  import * as Sse from "effect/unstable/encoding/Sse"
20
29
  import * as Headers from "effect/unstable/http/Headers"
21
30
  import * as HttpBody from "effect/unstable/http/HttpBody"
22
31
  import * as HttpClient from "effect/unstable/http/HttpClient"
23
32
  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"
33
+ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
34
+ import * as Socket from "effect/unstable/socket/Socket"
26
35
  import * as Errors from "./internal/errors.ts"
27
36
  import { OpenAiConfig } from "./OpenAiConfig.ts"
37
+ import * as OpenAiSchema from "./OpenAiSchema.ts"
28
38
 
29
39
  // =============================================================================
30
40
  // Service Interface
31
41
  // =============================================================================
32
42
 
33
43
  /**
34
- * The OpenAI client interface.
44
+ * Effect service interface for the handwritten OpenAI client.
45
+ *
46
+ * **Details**
47
+ *
48
+ * 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
49
  *
36
- * @since 1.0.0
37
50
  * @category models
51
+ * @since 4.0.0
38
52
  */
39
53
  export interface Service {
40
54
  /**
41
- * The underlying generated OpenAI client.
55
+ * The transformed HTTP client used by this service.
42
56
  */
43
- readonly client: Generated.OpenAiClient
57
+ readonly client: HttpClient.HttpClient
44
58
 
45
59
  /**
46
60
  * Create a response using the OpenAI responses endpoint.
47
61
  */
48
62
  readonly createResponse: (
49
- options: typeof Generated.CreateResponse.Encoded
63
+ options: typeof OpenAiSchema.CreateResponse.Encoded
50
64
  ) => Effect.Effect<
51
- [body: typeof Generated.Response.Type, response: HttpClientResponse.HttpClientResponse],
65
+ readonly [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
52
66
  AiError.AiError
53
67
  >
54
68
 
@@ -56,11 +70,11 @@ export interface Service {
56
70
  * Create a streaming response using the OpenAI responses endpoint.
57
71
  */
58
72
  readonly createResponseStream: (
59
- options: Omit<typeof Generated.CreateResponse.Encoded, "stream">
73
+ options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
60
74
  ) => Effect.Effect<
61
- [
75
+ readonly [
62
76
  response: HttpClientResponse.HttpClientResponse,
63
- stream: Stream.Stream<typeof Generated.ResponseStreamEvent.Type, AiError.AiError>
77
+ stream: Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
64
78
  ],
65
79
  AiError.AiError
66
80
  >
@@ -69,8 +83,8 @@ export interface Service {
69
83
  * Create embeddings using the OpenAI embeddings endpoint.
70
84
  */
71
85
  readonly createEmbedding: (
72
- options: typeof Generated.CreateEmbeddingRequest.Encoded
73
- ) => Effect.Effect<typeof Generated.CreateEmbeddingResponse.Type, AiError.AiError>
86
+ options: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
87
+ ) => Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError>
74
88
  }
75
89
 
76
90
  // =============================================================================
@@ -78,12 +92,21 @@ export interface Service {
78
92
  // =============================================================================
79
93
 
80
94
  /**
81
- * Service identifier for the OpenAI client.
95
+ * Service tag for the OpenAI client.
96
+ *
97
+ * **When to use**
82
98
  *
83
- * @since 1.0.0
84
- * @category service
99
+ * Use when accessing or providing the OpenAI client service through Effect's
100
+ * context.
101
+ *
102
+ * @see {@link make} for constructing an OpenAI client effectfully
103
+ * @see {@link layer} for providing a client from explicit options
104
+ * @see {@link layerConfig} for providing a client from `Config`
105
+ *
106
+ * @category services
107
+ * @since 4.0.0
85
108
  */
86
- export class OpenAiClient extends ServiceMap.Service<OpenAiClient, Service>()(
109
+ export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
87
110
  "@effect/ai-openai/OpenAiClient"
88
111
  ) {}
89
112
 
@@ -94,8 +117,8 @@ export class OpenAiClient extends ServiceMap.Service<OpenAiClient, Service>()(
94
117
  /**
95
118
  * Options for configuring the OpenAI client.
96
119
  *
97
- * @since 1.0.0
98
- * @category models
120
+ * @category options
121
+ * @since 4.0.0
99
122
  */
100
123
  export type Options = {
101
124
  /**
@@ -138,74 +161,106 @@ const RedactedOpenAiHeaders = {
138
161
  /**
139
162
  * Creates an OpenAI client service with the given options.
140
163
  *
141
- * @since 1.0.0
164
+ * **When to use**
165
+ *
166
+ * Use when you need the OpenAI client service value inside an effect.
167
+ *
168
+ * **Details**
169
+ *
170
+ * The returned service uses the current `HttpClient`, prepends `apiUrl` or
171
+ * `https://api.openai.com/v1`, adds the bearer token and optional OpenAI
172
+ * organization/project headers, accepts JSON responses, filters for successful
173
+ * HTTP statuses, and applies `transformClient` when provided.
174
+ *
175
+ * **Gotchas**
176
+ *
177
+ * A scoped `OpenAiConfig.withClientTransform` is applied when request helpers
178
+ * run, after the `transformClient` option supplied to `make`.
179
+ *
180
+ * @see {@link layer} for providing this client from explicit options
181
+ * @see {@link layerConfig} for loading client settings from `Config`
182
+ *
142
183
  * @category constructors
184
+ * @since 4.0.0
143
185
  */
144
186
  export const make = Effect.fnUntraced(
145
- function*(options: Options): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
187
+ function*(
188
+ options: Options
189
+ ): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
146
190
  const baseClient = yield* HttpClient.HttpClient
191
+ const apiUrl = options.apiUrl ?? "https://api.openai.com/v1"
147
192
 
148
193
  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)
194
+ HttpClient.mapRequest(Function.flow(
195
+ HttpClientRequest.prependUrl(apiUrl),
196
+ options.apiKey
197
+ ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
198
+ : identity,
199
+ options.organizationId
200
+ ? HttpClientRequest.setHeader(
201
+ RedactedOpenAiHeaders.OpenAiOrganization,
202
+ Redacted.value(options.organizationId)
203
+ )
204
+ : identity,
205
+ options.projectId
206
+ ? HttpClientRequest.setHeader(
207
+ RedactedOpenAiHeaders.OpenAiProject,
208
+ Redacted.value(options.projectId)
209
+ )
210
+ : identity,
211
+ HttpClientRequest.acceptJson
212
+ )),
213
+ HttpClient.filterStatusOk,
214
+ options.transformClient
171
215
  ? options.transformClient
172
216
  : identity
173
217
  )
174
218
 
175
- const httpClientOk = HttpClient.filterStatusOk(httpClient)
219
+ const resolveHttpClient = Effect.map(
220
+ OpenAiConfig.getOrUndefined,
221
+ (config) =>
222
+ Predicate.isNotUndefined(config?.transformClient)
223
+ ? config.transformClient(httpClient)
224
+ : httpClient
225
+ )
176
226
 
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
- })
227
+ const decodeResponse = HttpClientResponse.schemaBodyJson(OpenAiSchema.Response)
186
228
 
187
229
  const createResponse = (
188
- payload: typeof Generated.CreateResponse.Encoded
230
+ payload: typeof OpenAiSchema.CreateResponse.Encoded
189
231
  ): Effect.Effect<
190
- [body: typeof Generated.Response.Type, response: HttpClientResponse.HttpClientResponse],
232
+ [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
191
233
  AiError.AiError
192
234
  > =>
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
- )
235
+ Effect.flatMap(resolveHttpClient, (client) =>
236
+ client.execute(
237
+ HttpClientRequest.post("/responses", {
238
+ body: HttpBody.jsonUnsafe(payload)
239
+ })
240
+ ).pipe(
241
+ Effect.flatMap((response) =>
242
+ decodeResponse(response).pipe(
243
+ Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [
244
+ body,
245
+ response
246
+ ])
247
+ )
248
+ ),
249
+ Effect.catchTags({
250
+ HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"),
251
+ SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse"))
252
+ })
253
+ ))
199
254
 
200
255
  const buildResponseStream = (
201
256
  response: HttpClientResponse.HttpClientResponse
202
257
  ): [
203
258
  HttpClientResponse.HttpClientResponse,
204
- Stream.Stream<typeof Generated.ResponseStreamEvent.Type, AiError.AiError>
259
+ Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
205
260
  ] => {
206
261
  const stream = response.stream.pipe(
207
262
  Stream.decodeText(),
208
- Stream.pipeThroughChannel(Sse.decodeDataSchema(Generated.ResponseStreamEvent)),
263
+ Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)),
209
264
  Stream.takeUntil((event) =>
210
265
  event.data.type === "response.completed" ||
211
266
  event.data.type === "response.incomplete"
@@ -217,35 +272,48 @@ export const make = Effect.fnUntraced(
217
272
  HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")),
218
273
  SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createResponseStream"))
219
274
  })
220
- ) as any
275
+ )
221
276
  return [response, stream]
222
277
  }
223
278
 
224
279
  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
- )
280
+ Effect.contextWith((services) => {
281
+ const socket = Context.getOrUndefined(services, OpenAiSocket)
282
+ if (socket) return socket.createResponseStream(payload)
283
+ return Effect.flatMap(resolveHttpClient, (client) =>
284
+ client.execute(
285
+ HttpClientRequest.post("/responses", {
286
+ body: HttpBody.jsonUnsafe({ ...payload, stream: true })
287
+ })
288
+ ).pipe(
289
+ Effect.map(buildResponseStream),
290
+ Effect.catchTag(
291
+ "HttpClientError",
292
+ (error) => Errors.mapHttpClientError(error, "createResponseStream")
293
+ )
294
+ ))
295
+ })
296
+
297
+ const decodeEmbedding = HttpClientResponse.schemaBodyJson(OpenAiSchema.CreateEmbeddingResponse)
236
298
 
237
299
  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
- )
300
+ payload: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
301
+ ): Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError> =>
302
+ Effect.flatMap(resolveHttpClient, (client) =>
303
+ client.execute(
304
+ HttpClientRequest.post("/embeddings", {
305
+ body: HttpBody.jsonUnsafe(payload)
306
+ })
307
+ ).pipe(
308
+ Effect.flatMap(decodeEmbedding),
309
+ Effect.catchTags({
310
+ HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"),
311
+ SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding"))
312
+ })
313
+ ))
246
314
 
247
315
  return OpenAiClient.of({
248
- client,
316
+ client: httpClient,
249
317
  createResponse,
250
318
  createResponseStream,
251
319
  createEmbedding
@@ -264,24 +332,45 @@ export const make = Effect.fnUntraced(
264
332
  /**
265
333
  * Creates a layer for the OpenAI client with the given options.
266
334
  *
267
- * @since 1.0.0
335
+ * **When to use**
336
+ *
337
+ * Use when you already have explicit `Options` values, such as an API key or
338
+ * custom API URL, and want to provide `OpenAiClient` as a `Layer`.
339
+ *
340
+ * @see {@link make} for constructing the client service effectfully
341
+ * @see {@link layerConfig} for loading client settings from `Config`
342
+ *
268
343
  * @category layers
344
+ * @since 4.0.0
269
345
  */
270
346
  export const layer = (options: Options): Layer.Layer<OpenAiClient, never, HttpClient.HttpClient> =>
271
347
  Layer.effect(OpenAiClient, make(options))
272
348
 
273
349
  /**
274
- * Creates a layer for the OpenAI client, loading the requisite configuration
275
- * via Effect's `Config` module.
350
+ * Creates a layer for the OpenAI client from provided `Config` values.
351
+ *
352
+ * **When to use**
353
+ *
354
+ * Use when you need client settings for OpenAI-compatible APIs to be read from
355
+ * Effect `Config` values while providing `OpenAiClient` as a `Layer`.
356
+ *
357
+ * **Details**
358
+ *
359
+ * Only config values supplied in `options` are loaded. Omitted fields are
360
+ * passed to `make` as `undefined`, and `transformClient` is forwarded as a
361
+ * plain option.
362
+ *
363
+ * @see {@link make} for constructing the client service effectfully
364
+ * @see {@link layer} for providing the client from already-resolved options
276
365
  *
277
- * @since 1.0.0
278
366
  * @category layers
367
+ * @since 4.0.0
279
368
  */
280
369
  export const layerConfig = (options?: {
281
370
  /**
282
371
  * The config value to load for the API key.
283
372
  */
284
- readonly apiKey?: Config.Config<Redacted.Redacted<string>> | undefined
373
+ readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
285
374
 
286
375
  /**
287
376
  * The config value to load for the API URL.
@@ -291,12 +380,12 @@ export const layerConfig = (options?: {
291
380
  /**
292
381
  * The config value to load for the organization ID.
293
382
  */
294
- readonly organizationId?: Config.Config<Redacted.Redacted<string>> | undefined
383
+ readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
295
384
 
296
385
  /**
297
386
  * The config value to load for the project ID.
298
387
  */
299
- readonly projectId?: Config.Config<Redacted.Redacted<string>> | undefined
388
+ readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
300
389
 
301
390
  /**
302
391
  * Optional transformer for the HTTP client.
@@ -327,3 +416,315 @@ export const layerConfig = (options?: {
327
416
  })
328
417
  })
329
418
  )
419
+
420
+ // =============================================================================
421
+ // Websocket mode
422
+ // =============================================================================
423
+
424
+ /**
425
+ * Response stream event emitted by the OpenAI Responses API.
426
+ *
427
+ * @category Events
428
+ * @since 4.0.0
429
+ */
430
+ export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type
431
+
432
+ /**
433
+ * Service for creating OpenAI response streams over a WebSocket connection.
434
+ *
435
+ * **When to use**
436
+ *
437
+ * Use when you need direct access to the WebSocket-backed response streaming
438
+ * service rather than wrapping an effect with WebSocket mode.
439
+ *
440
+ * **Details**
441
+ *
442
+ * `createResponseStream` sends a `response.create` message over the WebSocket
443
+ * connection and returns an HTTP response together with a stream of
444
+ * `ResponseStreamEvent` values.
445
+ *
446
+ * **Gotchas**
447
+ *
448
+ * WebSocket response streams are serialized to one request at a time by the
449
+ * shared socket service.
450
+ *
451
+ * @see {@link withWebSocketMode} for enabling WebSocket mode for one effect
452
+ * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer
453
+ *
454
+ * @category Websocket mode
455
+ * @since 4.0.0
456
+ */
457
+ export class OpenAiSocket extends Context.Service<OpenAiSocket, {
458
+ /**
459
+ * Create a streaming response using the OpenAI responses endpoint.
460
+ */
461
+ readonly createResponseStream: (
462
+ options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
463
+ ) => Effect.Effect<
464
+ readonly [
465
+ response: HttpClientResponse.HttpClientResponse,
466
+ stream: Stream.Stream<ResponseStreamEvent, AiError.AiError>
467
+ ],
468
+ AiError.AiError
469
+ >
470
+ }>()("@effect/ai-openai/OpenAiClient/OpenAiSocket") {}
471
+
472
+ const makeSocket = Effect.gen(function*() {
473
+ const client = yield* OpenAiClient
474
+ const tracker = yield* ResponseIdTracker.make
475
+ const socketScope = yield* Effect.scope
476
+ const makeRequest = Effect.flatMap(
477
+ OpenAiConfig.getOrUndefined,
478
+ (config) => {
479
+ const httpClient = Predicate.isNotUndefined(config?.transformClient)
480
+ ? config.transformClient(client.client)
481
+ : client.client
482
+ return Effect.orDie(httpClient.preprocess(HttpClientRequest.post("/responses")))
483
+ }
484
+ )
485
+ const makeWebSocket = yield* Socket.WebSocketConstructor
486
+
487
+ const decoder = new TextDecoder()
488
+
489
+ const queueRef: RcRef.RcRef<
490
+ {
491
+ readonly send: (message: typeof OpenAiSchema.CreateResponse.Encoded) => Effect.Effect<void, AiError.AiError>
492
+ readonly incoming: Queue.Dequeue<ResponseStreamEvent, AiError.AiError>
493
+ }
494
+ > = yield* RcRef.make({
495
+ idleTimeToLive: 60_000,
496
+ acquire: Effect.gen(function*() {
497
+ const scope = yield* Effect.scope
498
+ const request = yield* makeRequest
499
+ const socket = yield* Socket.makeWebSocket(request.url.replace(/^http/, "ws")).pipe(
500
+ Effect.provideService(Socket.WebSocketConstructor, (url) =>
501
+ makeWebSocket(url, {
502
+ headers: request.headers
503
+ } as any))
504
+ )
505
+ const write = yield* socket.writer
506
+
507
+ yield* Scope.addFinalizerExit(scope, () => {
508
+ tracker.clearUnsafe()
509
+ return Effect.void
510
+ })
511
+
512
+ const incoming = yield* Queue.unbounded<ResponseStreamEvent, AiError.AiError>()
513
+ const send = (message: typeof OpenAiSchema.CreateResponse.Encoded) =>
514
+ write(JSON.stringify({
515
+ type: "response.create",
516
+ ...message
517
+ })).pipe(
518
+ Effect.mapError((_error) =>
519
+ AiError.make({
520
+ module: "OpenAiClient",
521
+ method: "createResponseStream",
522
+ reason: new AiError.NetworkError({
523
+ reason: "TransportError",
524
+ request: {
525
+ method: "POST",
526
+ url: request.url,
527
+ urlParams: [],
528
+ hash: undefined,
529
+ headers: request.headers
530
+ },
531
+ description: "Failed to send message over WebSocket"
532
+ })
533
+ })
534
+ )
535
+ )
536
+
537
+ yield* socket.runRaw((msg) => {
538
+ const text = typeof msg === "string" ? msg : decoder.decode(msg)
539
+ try {
540
+ const event = decodeEvent(text)
541
+ if (event.type === "error" && "status" in event) {
542
+ const status = Number(event.status)
543
+ const error = "error" in event ? event.error as typeof ErrorEvent.Type.error : event
544
+ const json = JSON.stringify(error)
545
+ return Effect.fail(
546
+ AiError.make({
547
+ module: "OpenAiClient",
548
+ method: "createResponseStream",
549
+ reason: AiError.reasonFromHttpStatus({
550
+ description: json,
551
+ status: isNaN(status) ? errorTypeToStatus[error.type] ?? 500 : status,
552
+ metadata: error as any,
553
+ http: {
554
+ body: json,
555
+ request: {
556
+ method: "POST",
557
+ url: request.url,
558
+ urlParams: [],
559
+ hash: undefined,
560
+ headers: request.headers
561
+ }
562
+ }
563
+ })
564
+ })
565
+ )
566
+ }
567
+ Queue.offerUnsafe(incoming, event)
568
+ } catch {}
569
+ }).pipe(
570
+ Effect.catchTag("SocketError", (error) =>
571
+ AiError.make({
572
+ module: "OpenAiClient",
573
+ method: "createResponseStream",
574
+ reason: new AiError.NetworkError({
575
+ reason: "TransportError",
576
+ request: {
577
+ method: "POST",
578
+ url: request.url,
579
+ urlParams: [],
580
+ hash: undefined,
581
+ headers: request.headers
582
+ },
583
+ description: error.message
584
+ })
585
+ })),
586
+ Effect.catchCause((cause) => Queue.failCause(incoming, cause)),
587
+ Effect.ensuring(Effect.forkIn(RcRef.invalidate(queueRef), socketScope, {
588
+ startImmediately: true
589
+ })),
590
+ Effect.forkScoped({ startImmediately: true })
591
+ )
592
+
593
+ return { send, incoming } as const
594
+ })
595
+ })
596
+
597
+ // Prime the websocket
598
+ yield* Effect.scoped(RcRef.get(queueRef))
599
+
600
+ // Websocket mode only allows one request at a time
601
+ const semaphore = Semaphore.makeUnsafe(1)
602
+ const request = yield* makeRequest
603
+
604
+ return OpenAiSocket.context({
605
+ createResponseStream(options) {
606
+ const stream = Stream.unwrap(Effect.gen(function*() {
607
+ const scope = yield* Effect.scope
608
+ yield* Effect.acquireRelease(
609
+ semaphore.take(1),
610
+ () => semaphore.release(1),
611
+ { interruptible: true }
612
+ )
613
+ const { send, incoming } = yield* RcRef.get(queueRef)
614
+ let done = false
615
+
616
+ yield* Scope.addFinalizerExit(
617
+ scope,
618
+ () => done ? Effect.void : RcRef.invalidate(queueRef)
619
+ )
620
+
621
+ yield* send(options).pipe(
622
+ Effect.forkScoped({ startImmediately: true })
623
+ )
624
+
625
+ return Stream.fromQueue(incoming).pipe(
626
+ Stream.takeUntil((e) => {
627
+ done = e.type === "response.completed" || e.type === "response.incomplete"
628
+ return done
629
+ })
630
+ )
631
+ }))
632
+
633
+ return Effect.succeed([
634
+ HttpClientResponse.fromWeb(request, new Response()),
635
+ stream
636
+ ])
637
+ }
638
+ }).pipe(
639
+ Context.add(ResponseIdTracker.ResponseIdTracker, tracker)
640
+ )
641
+ })
642
+
643
+ const ErrorEvent = Schema.Struct({
644
+ type: Schema.Literal("error"),
645
+ status: Schema.Number.pipe(
646
+ Schema.withDecodingDefault(Effect.succeed(500))
647
+ ),
648
+ error: Schema.Struct({
649
+ type: Schema.String,
650
+ message: Schema.String
651
+ })
652
+ })
653
+
654
+ const errorTypeToStatus: Record<string, number> = {
655
+ invalid_request_error: 400,
656
+ invalid_api_key_error: 401,
657
+ insufficient_quota_error: 429,
658
+ rate_limit_error: 429,
659
+ service_unavailable_error: 503
660
+ }
661
+
662
+ const AllEvents = Schema.Union([ErrorEvent, OpenAiSchema.ResponseStreamEvent])
663
+ const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents))
664
+
665
+ /**
666
+ * Uses OpenAI's WebSocket mode for response streams within the provided effect.
667
+ *
668
+ * **When to use**
669
+ *
670
+ * Use to enable WebSocket mode around one effect that creates OpenAI response
671
+ * streams.
672
+ *
673
+ * **Gotchas**
674
+ *
675
+ * This only works with the following WebSocket constructor layers:
676
+ *
677
+ * - `NodeSocket.layerWebSocketConstructorWS`
678
+ * - `BunSocket.layerWebSocketConstructor`
679
+ *
680
+ * These constructor layers support the non-standard options needed to set the
681
+ * Authorization header.
682
+ *
683
+ * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer
684
+ * @see {@link OpenAiSocket} for direct access to the WebSocket-backed streaming service
685
+ *
686
+ * @category Websocket mode
687
+ * @since 4.0.0
688
+ */
689
+ export const withWebSocketMode = <A, E, R>(
690
+ effect: Effect.Effect<A, E, R>
691
+ ): Effect.Effect<
692
+ A,
693
+ E,
694
+ Exclude<R, OpenAiSocket | ResponseIdTracker.ResponseIdTracker> | OpenAiClient | Socket.WebSocketConstructor
695
+ > =>
696
+ Effect.scopedWith((scope) =>
697
+ Effect.flatMap(
698
+ Scope.provide(makeSocket, scope),
699
+ (services) => Effect.provideContext(effect, services)
700
+ )
701
+ )
702
+
703
+ /**
704
+ * Uses OpenAI's websocket mode for all responses that use the Layer.
705
+ *
706
+ * **When to use**
707
+ *
708
+ * Use to provide WebSocket mode through layer composition for effects that use
709
+ * OpenAI response streaming.
710
+ *
711
+ * **Gotchas**
712
+ *
713
+ * This only works with the following WebSocket constructor layers:
714
+ *
715
+ * - `NodeSocket.layerWebSocketConstructorWS`
716
+ * - `BunSocket.layerWebSocketConstructor`
717
+ *
718
+ * These constructor layers support the non-standard options needed to set the
719
+ * Authorization header.
720
+ *
721
+ * @see {@link withWebSocketMode} for enabling WebSocket mode around a single effect
722
+ *
723
+ * @category Websocket mode
724
+ * @since 4.0.0
725
+ */
726
+ export const layerWebSocketMode: Layer.Layer<
727
+ OpenAiSocket | ResponseIdTracker.ResponseIdTracker,
728
+ never,
729
+ OpenAiClient | Socket.WebSocketConstructor
730
+ > = Layer.effectContext(makeSocket)