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

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