1/**
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`.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as AiError from "effect/ai/AiError"
13import * as ResponseIdTracker from "effect/ai/ResponseIdTracker"
14import * as Array from "effect/Array"
15import * as Cause from "effect/Cause"
16import type * as Config from "effect/Config"
17import * as Context from "effect/Context"
18import * as Effect from "effect/Effect"
19import * as Sse from "effect/encoding/Sse"
20import { identity } from "effect/Function"
21import * as Function from "effect/Function"
22import * as Headers from "effect/http/Headers"
23import * as HttpBody from "effect/http/HttpBody"
24import * as HttpClient from "effect/http/HttpClient"
25import * as HttpClientRequest from "effect/http/HttpClientRequest"
26import * as HttpClientResponse from "effect/http/HttpClientResponse"
27import * as Layer from "effect/Layer"
28import * as Predicate from "effect/Predicate"
29import * as Queue from "effect/Queue"
30import * as RcRef from "effect/RcRef"
31import * as Redacted from "effect/Redacted"
32import * as Schema from "effect/Schema"
33import * as Scope from "effect/Scope"
34import * as Semaphore from "effect/Semaphore"
35import * as Socket from "effect/socket/Socket"
36import * as Stream from "effect/Stream"
37import * as Errors from "./internal/errors.ts"
38import { OpenAiConfig } from "./OpenAiConfig.ts"
39import * as OpenAiSchema from "./OpenAiSchema.ts"
40
41// =============================================================================
42// Service Interface
43// =============================================================================
44
45/**
46 * Effect service interface for the handwritten OpenAI client.
47 *
48 * **Details**
49 *
50 * 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`.
51 *
52 * @stability unstable
53 * @category services
54 * @since 4.0.0
55 */
56export interface Service {
57 /**
58 * The transformed HTTP client used by this service.
59 */
60 readonly client: HttpClient.HttpClient
61
62 /**
63 * Create a response using the OpenAI responses endpoint.
64 */
65 readonly createResponse: (
66 options: typeof OpenAiSchema.CreateResponse.Encoded
67 ) => Effect.Effect<
68 readonly [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
69 AiError.AiError
70 >
71
72 /**
73 * Create a streaming response using the OpenAI responses endpoint.
74 */
75 readonly createResponseStream: (
76 options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
77 ) => Effect.Effect<
78 readonly [
79 response: HttpClientResponse.HttpClientResponse,
80 stream: Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
81 ],
82 AiError.AiError
83 >
84
85 /**
86 * Create embeddings using the OpenAI embeddings endpoint.
87 */
88 readonly createEmbedding: (
89 options: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
90 ) => Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError>
91}
92
93// =============================================================================
94// Service Identifier
95// =============================================================================
96
97/**
98 * Service tag for the OpenAI client.
99 *
100 * **When to use**
101 *
102 * Use when accessing or providing the OpenAI client service through Effect's
103 * context.
104 *
105 * @see {@link make} for constructing an OpenAI client effectfully
106 * @see {@link layer} for providing a client from explicit options
107 * @see {@link layerConfig} for providing a client from `Config`
108 *
109 * @stability unstable
110 * @category services
111 * @since 4.0.0
112 */
113export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
114 "@effect/ai-openai/OpenAiClient"
115) {}
116
117// =============================================================================
118// Options
119// =============================================================================
120
121/**
122 * Options for configuring the OpenAI client.
123 *
124 * @stability unstable
125 * @category options
126 * @since 4.0.0
127 */
128export type Options = {
129 /**
130 * The OpenAI API key.
131 */
132 readonly apiKey?: Redacted.Redacted<string> | undefined
133
134 /**
135 * The base URL for the OpenAI API.
136 *
137 * @default "https://api.openai.com/v1"
138 */
139 readonly apiUrl?: string | undefined
140
141 /**
142 * Optional organization ID for multi-org accounts.
143 */
144 readonly organizationId?: Redacted.Redacted<string> | undefined
145
146 /**
147 * Optional project ID for project-scoped requests.
148 */
149 readonly projectId?: Redacted.Redacted<string> | undefined
150
151 /**
152 * Optional transformer for the HTTP client.
153 */
154 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
155}
156
157// =============================================================================
158// Constructor
159// =============================================================================
160
161const RedactedOpenAiHeaders = {
162 OpenAiOrganization: "OpenAI-Organization",
163 OpenAiProject: "OpenAI-Project"
164}
165
166const withRedactedHeaders = Effect.updateService(
167 Headers.CurrentRedactedNames,
168 Array.appendAll(Object.values(RedactedOpenAiHeaders))
169)
170
171/**
172 * Creates an OpenAI client service with the given options.
173 *
174 * **When to use**
175 *
176 * Use when you need the OpenAI client service value inside an effect.
177 *
178 * **Details**
179 *
180 * The returned service uses the current `HttpClient`, prepends `apiUrl` or
181 * `https://api.openai.com/v1`, adds the bearer token and optional OpenAI
182 * organization/project headers, accepts JSON responses, filters for successful
183 * HTTP statuses, and applies `transformClient` when provided.
184 *
185 * **Gotchas**
186 *
187 * A scoped `OpenAiConfig.withClientTransform` is applied when request helpers
188 * run, after the `transformClient` option supplied to `make`.
189 *
190 * @see {@link layer} for providing this client from explicit options
191 * @see {@link layerConfig} for loading client settings from `Config`
192 *
193 * @stability unstable
194 * @category constructors
195 * @since 4.0.0
196 */
197export const make = Effect.fnUntraced(
198 function*(
199 options: Options
200 ): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
201 const baseClient = yield* HttpClient.HttpClient
202 const apiUrl = options.apiUrl ?? "https://api.openai.com/v1"
203
204 const httpClient = baseClient.pipe(
205 HttpClient.mapRequest(Function.flow(
206 HttpClientRequest.prependUrl(apiUrl),
207 options.apiKey
208 ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
209 : identity,
210 options.organizationId
211 ? HttpClientRequest.setHeader(
212 RedactedOpenAiHeaders.OpenAiOrganization,
213 Redacted.value(options.organizationId)
214 )
215 : identity,
216 options.projectId
217 ? HttpClientRequest.setHeader(
218 RedactedOpenAiHeaders.OpenAiProject,
219 Redacted.value(options.projectId)
220 )
221 : identity,
222 HttpClientRequest.acceptJson
223 )),
224 HttpClient.filterStatusOk,
225 options.transformClient
226 ? options.transformClient
227 : identity
228 )
229
230 const resolveHttpClient = Effect.map(
231 OpenAiConfig.getOrUndefined,
232 (config) =>
233 Predicate.isNotUndefined(config?.transformClient)
234 ? config.transformClient(httpClient)
235 : httpClient
236 )
237
238 const decodeResponse = HttpClientResponse.schemaBodyJson(OpenAiSchema.Response)
239
240 const createResponse = (
241 payload: typeof OpenAiSchema.CreateResponse.Encoded
242 ): Effect.Effect<
243 [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
244 AiError.AiError
245 > =>
246 resolveHttpClient.pipe(
247 Effect.flatMap((client) =>
248 client.execute(HttpClientRequest.post("/responses", {
249 body: HttpBody.jsonUnsafe(payload)
250 })).pipe(
251 Effect.flatMap((response) =>
252 decodeResponse(response).pipe(
253 Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [
254 body,
255 response
256 ])
257 )
258 ),
259 Effect.catchTags({
260 HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"),
261 SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse"))
262 })
263 )
264 ),
265 withRedactedHeaders
266 )
267
268 const buildResponseStream = (
269 response: HttpClientResponse.HttpClientResponse
270 ): [
271 HttpClientResponse.HttpClientResponse,
272 Stream.Stream<typeof OpenAiSchema.ResponseStreamEvent.Type, AiError.AiError>
273 ] => {
274 const stream = response.stream.pipe(
275 Stream.decodeText(),
276 Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)),
277 Stream.takeUntil((event) =>
278 event.data.type === "response.completed" ||
279 event.data.type === "response.incomplete" ||
280 event.data.type === "response.failed"
281 ),
282 Stream.map((event) => event.data),
283 Stream.catchTags({
284 // TODO: handle SSE retries
285 Retry: (error) => Stream.die(error),
286 SseError: (error) => Stream.fail(Errors.mapSseError(error, "createResponseStream")),
287 HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")),
288 SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createResponseStream"))
289 })
290 )
291 return [response, stream]
292 }
293
294 const createResponseStream: Service["createResponseStream"] = (payload) =>
295 Effect.contextWith((services) => {
296 const socket = Context.getOrUndefined(services, OpenAiSocket)
297 if (socket) return socket.createResponseStream(payload)
298 return resolveHttpClient.pipe(
299 Effect.flatMap((client) =>
300 client.execute(HttpClientRequest.post("/responses", {
301 body: HttpBody.jsonUnsafe({ ...payload, stream: true })
302 })).pipe(
303 Effect.map(buildResponseStream),
304 Effect.catchTag(
305 "HttpClientError",
306 (error) => Errors.mapHttpClientError(error, "createResponseStream")
307 )
308 )
309 ),
310 withRedactedHeaders
311 )
312 })
313
314 const decodeEmbedding = HttpClientResponse.schemaBodyJson(OpenAiSchema.CreateEmbeddingResponse)
315
316 const createEmbedding = (
317 payload: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded
318 ): Effect.Effect<typeof OpenAiSchema.CreateEmbeddingResponse.Type, AiError.AiError> =>
319 resolveHttpClient.pipe(
320 Effect.flatMap((client) =>
321 client.execute(HttpClientRequest.post("/embeddings", {
322 body: HttpBody.jsonUnsafe(payload)
323 })).pipe(
324 Effect.flatMap(decodeEmbedding),
325 Effect.catchTags({
326 HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"),
327 SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding"))
328 })
329 )
330 ),
331 withRedactedHeaders
332 )
333
334 return OpenAiClient.of({
335 client: httpClient,
336 createResponse,
337 createResponseStream,
338 createEmbedding
339 })
340 },
341 withRedactedHeaders
342)
343
344// =============================================================================
345// Layers
346// =============================================================================
347
348/**
349 * Creates a layer for the OpenAI client with the given options.
350 *
351 * **When to use**
352 *
353 * Use when you already have explicit `Options` values, such as an API key or
354 * custom API URL, and want to provide `OpenAiClient` as a `Layer`.
355 *
356 * @see {@link make} for constructing the client service effectfully
357 * @see {@link layerConfig} for loading client settings from `Config`
358 *
359 * @stability unstable
360 * @category layers
361 * @since 4.0.0
362 */
363export const layer = (options: Options): Layer.Layer<OpenAiClient, never, HttpClient.HttpClient> =>
364 Layer.effect(OpenAiClient, make(options))
365
366/**
367 * Creates a layer for the OpenAI client from provided `Config` values.
368 *
369 * **When to use**
370 *
371 * Use when you need client settings for OpenAI-compatible APIs to be read from
372 * Effect `Config` values while providing `OpenAiClient` as a `Layer`.
373 *
374 * **Details**
375 *
376 * Only config values supplied in `options` are loaded. Omitted fields are
377 * passed to `make` as `undefined`, and `transformClient` is forwarded as a
378 * plain option.
379 *
380 * @see {@link make} for constructing the client service effectfully
381 * @see {@link layer} for providing the client from already-resolved options
382 *
383 * @stability unstable
384 * @category layers
385 * @since 4.0.0
386 */
387export const layerConfig = (options?: {
388 /**
389 * The config value to load for the API key.
390 */
391 readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
392
393 /**
394 * The config value to load for the API URL.
395 */
396 readonly apiUrl?: Config.Config<string> | undefined
397
398 /**
399 * The config value to load for the organization ID.
400 */
401 readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
402
403 /**
404 * The config value to load for the project ID.
405 */
406 readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
407
408 /**
409 * Optional transformer for the HTTP client.
410 */
411 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
412}): Layer.Layer<OpenAiClient, Config.ConfigError, HttpClient.HttpClient> =>
413 Layer.effect(
414 OpenAiClient,
415 Effect.gen(function*() {
416 const apiKey = Predicate.isNotUndefined(options?.apiKey)
417 ? yield* options.apiKey :
418 undefined
419 const apiUrl = Predicate.isNotUndefined(options?.apiUrl)
420 ? yield* options.apiUrl :
421 undefined
422 const organizationId = Predicate.isNotUndefined(options?.organizationId)
423 ? yield* options.organizationId
424 : undefined
425 const projectId = Predicate.isNotUndefined(options?.projectId)
426 ? yield* options.projectId :
427 undefined
428 return yield* make({
429 apiKey,
430 apiUrl,
431 organizationId,
432 projectId,
433 transformClient: options?.transformClient
434 })
435 })
436 )
437
438// =============================================================================
439// Websocket mode
440// =============================================================================
441
442/**
443 * Response stream event emitted by the OpenAI Responses API.
444 *
445 * @stability unstable
446 * @category models
447 * @since 4.0.0
448 */
449export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type
450
451/**
452 * Service for creating OpenAI response streams over a WebSocket connection.
453 *
454 * **When to use**
455 *
456 * Use when you need direct access to the WebSocket-backed response streaming
457 * service rather than wrapping an effect with WebSocket mode.
458 *
459 * **Details**
460 *
461 * `createResponseStream` sends a `response.create` message over the WebSocket
462 * connection and returns an HTTP response together with a stream of
463 * `ResponseStreamEvent` values.
464 *
465 * **Gotchas**
466 *
467 * WebSocket response streams are serialized to one request at a time by the
468 * shared socket service.
469 *
470 * @see {@link withWebSocketMode} for enabling WebSocket mode for one effect
471 * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer
472 *
473 * @stability unstable
474 * @category services
475 * @since 4.0.0
476 */
477export class OpenAiSocket extends Context.Service<OpenAiSocket, {
478 /**
479 * Create a streaming response using the OpenAI responses endpoint.
480 */
481 readonly createResponseStream: (
482 options: Omit<typeof OpenAiSchema.CreateResponse.Encoded, "stream">
483 ) => Effect.Effect<
484 readonly [
485 response: HttpClientResponse.HttpClientResponse,
486 stream: Stream.Stream<ResponseStreamEvent, AiError.AiError>
487 ],
488 AiError.AiError
489 >
490}>()("@effect/ai-openai/OpenAiClient/OpenAiSocket") {}
491
492const makeSocket = Effect.gen(function*() {
493 const client = yield* OpenAiClient
494 const tracker = yield* ResponseIdTracker.make
495 const socketScope = yield* Effect.scope
496 const makeRequest = Effect.flatMap(
497 OpenAiConfig.getOrUndefined,
498 (config) => {
499 const httpClient = Predicate.isNotUndefined(config?.transformClient)
500 ? config.transformClient(client.client)
501 : client.client
502 return Effect.orDie(httpClient.preprocess(HttpClientRequest.post("/responses")))
503 }
504 )
505 const makeWebSocket = yield* Socket.WebSocketConstructor
506
507 const decoder = new TextDecoder()
508
509 // The response currently in flight, or undefined between turns. Each turn
510 // owns its queue; the reader ends or fails it, so a turn that is still
511 // current when its consumer leaves was abandoned mid-response.
512 type Turn = Queue.Queue<ResponseStreamEvent, AiError.AiError | Cause.Done>
513 type SocketConnection = {
514 readonly send: (message: typeof OpenAiSchema.CreateResponse.Encoded) => Effect.Effect<void, AiError.AiError>
515 current: Turn | undefined
516 }
517
518 const queueRef: RcRef.RcRef<SocketConnection> = yield* RcRef.make({
519 idleTimeToLive: 60_000,
520 acquire: Effect.gen(function*() {
521 const scope = yield* Effect.scope
522 const request = yield* makeRequest
523 const socket = yield* Socket.makeWebSocket(request.url.replace(/^http/, "ws")).pipe(
524 Effect.provideService(Socket.WebSocketConstructor, (url) =>
525 makeWebSocket(url, {
526 headers: request.headers
527 }))
528 )
529 const writer = yield* socket.writer
530
531 yield* Scope.addFinalizerExit(scope, () => {
532 tracker.clearUnsafe()
533 return Effect.void
534 })
535
536 const connection: SocketConnection = {
537 current: undefined,
538 send: (message) =>
539 writer.write(JSON.stringify({
540 type: "response.create",
541 ...message
542 })).pipe(
543 Effect.mapError((_error) =>
544 AiError.make({
545 module: "OpenAiClient",
546 method: "createResponseStream",
547 reason: new AiError.NetworkError({
548 reason: "TransportError",
549 request: {
550 method: "POST",
551 url: request.url,
552 urlParams: [],
553 hash: undefined,
554 headers: request.headers
555 },
556 description: "Failed to send message over WebSocket"
557 })
558 })
559 )
560 )
561 }
562
563 const handleMessage = (msg: Uint8Array | string): void => {
564 const turn = connection.current
565 if (turn === undefined) return
566 let event: typeof AllEvents.Type
567 try {
568 event = decodeEvent(typeof msg === "string" ? msg : decoder.decode(msg))
569 } catch {
570 return
571 }
572 if (event.type === "error") {
573 const status = "status" in event ? Number(event.status) : NaN
574 const error = ("error" in event ? event.error : event) as {
575 readonly type?: string
576 readonly code?: string
577 readonly message: string
578 }
579 const errorType = error.code ??
580 (error.type === "api_error" && /^gRPC error: Response with id=\S+ not found$/.test(error.message)
581 ? "previous_response_not_found"
582 : error.type ?? "unknown")
583 const json = JSON.stringify(error)
584 // LanguageModel retries `previous_response_not_found` with the full
585 // prompt on this socket. Other errors leave the turn current so its
586 // finalizer invalidates the connection, preserving the existing policy.
587 if (errorType === "previous_response_not_found") {
588 connection.current = undefined
589 }
590 Queue.failCauseUnsafe(
591 turn,
592 Cause.fail(AiError.make({
593 module: "OpenAiClient",
594 method: "createResponseStream",
595 reason: AiError.reasonFromHttpStatus({
596 description: json,
597 status: errorType === "previous_response_not_found"
598 ? 400
599 : isNaN(status)
600 ? Object.hasOwn(errorTypeToStatus, errorType)
601 ? errorTypeToStatus[errorType]
602 : 500
603 : status,
604 metadata: error as any,
605 http: {
606 body: json,
607 request: {
608 method: "POST",
609 url: request.url,
610 urlParams: [],
611 hash: undefined,
612 headers: request.headers
613 }
614 }
615 })
616 }))
617 )
618 return
619 }
620 Queue.offerUnsafe(turn, event)
621 if (
622 event.type === "response.completed" || event.type === "response.incomplete" ||
623 event.type === "response.failed"
624 ) {
625 connection.current = undefined
626 Queue.endUnsafe(turn)
627 }
628 }
629
630 yield* Effect.gen(function*() {
631 const { pull } = yield* socket.reader
632 while (true) {
633 const messages = yield* pull
634 for (let i = 0; i < messages.length; i++) {
635 handleMessage(messages[i])
636 }
637 }
638 }).pipe(
639 Effect.scoped,
640 Effect.catchTag("SocketError", (error) =>
641 AiError.make({
642 module: "OpenAiClient",
643 method: "createResponseStream",
644 reason: new AiError.NetworkError({
645 reason: "TransportError",
646 request: {
647 method: "POST",
648 url: request.url,
649 urlParams: [],
650 hash: undefined,
651 headers: request.headers
652 },
653 description: error.message
654 })
655 })),
656 Effect.catchCause((cause) =>
657 connection.current === undefined ? Effect.void : Queue.failCause(connection.current, cause)
658 ),
659 Effect.ensuring(Effect.forkIn(RcRef.invalidate(queueRef), socketScope, {
660 startImmediately: true
661 })),
662 Effect.forkScoped({ startImmediately: true })
663 )
664
665 return connection
666 })
667 })
668
669 // Prime the websocket
670 yield* Effect.scoped(RcRef.get(queueRef))
671
672 // Websocket mode only allows one request at a time
673 const semaphore = Semaphore.makeUnsafe(1)
674 const request = yield* makeRequest
675
676 return OpenAiSocket.context({
677 createResponseStream(options) {
678 const stream = Stream.unwrap(Effect.gen(function*() {
679 const scope = yield* Effect.scope
680 yield* Effect.acquireRelease(
681 semaphore.take(1),
682 () => semaphore.release(1),
683 { interruptible: true }
684 )
685 const connection = yield* RcRef.get(queueRef)
686 const turn: Turn = yield* Queue.unbounded<ResponseStreamEvent, AiError.AiError | Cause.Done>()
687 connection.current = turn
688
689 // Leaving while the response is still streaming makes the socket
690 // unusable for the next turn.
691 yield* Scope.addFinalizerExit(
692 scope,
693 () => connection.current === turn ? RcRef.invalidate(queueRef) : Effect.void
694 )
695
696 yield* connection.send(options).pipe(
697 Effect.forkScoped({ startImmediately: true })
698 )
699
700 return Stream.fromQueue(turn)
701 }))
702
703 return Effect.succeed([
704 HttpClientResponse.fromWeb(request, new Response()),
705 stream
706 ])
707 }
708 }).pipe(
709 Context.add(ResponseIdTracker.ResponseIdTracker, tracker)
710 )
711})
712
713const ErrorEvent = Schema.Struct({
714 type: Schema.Literal("error").pipe(
715 Schema.withDecodingDefault(Effect.succeed("error" as const))
716 ),
717 status: Schema.optionalKey(Schema.Int),
718 // xAI sends `code` in place of `type`, e.g. `previous_response_not_found`
719 error: Schema.Struct({
720 type: Schema.optional(Schema.String),
721 code: Schema.optional(Schema.String),
722 message: Schema.String
723 })
724})
725
726const errorTypeToStatus: Record<string, number> = {
727 invalid_request_error: 400,
728 invalid_api_key_error: 401,
729 insufficient_quota_error: 429,
730 rate_limit_error: 429,
731 service_unavailable_error: 503
732}
733
734const AllEvents = Schema.Union([ErrorEvent, OpenAiSchema.ResponseStreamEvent])
735const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents))
736
737/**
738 * Uses OpenAI's WebSocket mode for response streams within the provided effect.
739 *
740 * **When to use**
741 *
742 * Use to enable WebSocket mode around one effect that creates OpenAI response
743 * streams.
744 *
745 * **Gotchas**
746 *
747 * This only works with the following WebSocket constructor layers:
748 *
749 * - `NodeSocket.layerWebSocketConstructorWS`
750 * - `BunSocket.layerWebSocketConstructor`
751 *
752 * These constructor layers support the non-standard options needed to set the
753 * Authorization header.
754 *
755 * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer
756 * @see {@link OpenAiSocket} for direct access to the WebSocket-backed streaming service
757 *
758 * @stability unstable
759 * @category providing services
760 * @since 4.0.0
761 */
762export const withWebSocketMode = <A, E, R>(
763 effect: Effect.Effect<A, E, R>
764): Effect.Effect<
765 A,
766 E,
767 Exclude<R, OpenAiSocket | ResponseIdTracker.ResponseIdTracker> | OpenAiClient | Socket.WebSocketConstructor
768> =>
769 Effect.scopedWith((scope) =>
770 Effect.flatMap(
771 Scope.provide(makeSocket, scope),
772 (services) => Effect.provideContext(effect, services)
773 )
774 )
775
776/**
777 * Uses OpenAI's websocket mode for all responses that use the Layer.
778 *
779 * **When to use**
780 *
781 * Use to provide WebSocket mode through layer composition for effects that use
782 * OpenAI response streaming.
783 *
784 * **Gotchas**
785 *
786 * This only works with the following WebSocket constructor layers:
787 *
788 * - `NodeSocket.layerWebSocketConstructorWS`
789 * - `BunSocket.layerWebSocketConstructor`
790 *
791 * These constructor layers support the non-standard options needed to set the
792 * Authorization header.
793 *
794 * @see {@link withWebSocketMode} for enabling WebSocket mode around a single effect
795 *
796 * @stability unstable
797 * @category layers
798 * @since 4.0.0
799 */
800export const layerWebSocketMode: Layer.Layer<
801 OpenAiSocket | ResponseIdTracker.ResponseIdTracker,
802 never,
803 OpenAiClient | Socket.WebSocketConstructor
804> = Layer.effectContext(makeSocket)