@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.
- package/dist/Generated.d.ts +66734 -37723
- package/dist/Generated.d.ts.map +1 -1
- package/dist/Generated.js +1 -1
- package/dist/Generated.js.map +1 -1
- package/dist/OpenAiClient.d.ts +81 -25
- package/dist/OpenAiClient.d.ts.map +1 -1
- package/dist/OpenAiClient.js +220 -39
- package/dist/OpenAiClient.js.map +1 -1
- package/dist/OpenAiClientGenerated.d.ts +91 -0
- package/dist/OpenAiClientGenerated.d.ts.map +1 -0
- package/dist/OpenAiClientGenerated.js +84 -0
- package/dist/OpenAiClientGenerated.js.map +1 -0
- package/dist/OpenAiConfig.d.ts +45 -10
- package/dist/OpenAiConfig.d.ts.map +1 -1
- package/dist/OpenAiConfig.js +31 -7
- package/dist/OpenAiConfig.js.map +1 -1
- package/dist/OpenAiEmbeddingModel.d.ts +89 -0
- package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
- package/dist/OpenAiEmbeddingModel.js +121 -0
- package/dist/OpenAiEmbeddingModel.js.map +1 -0
- package/dist/OpenAiError.d.ts +168 -35
- package/dist/OpenAiError.d.ts.map +1 -1
- package/dist/OpenAiError.js +1 -1
- package/dist/OpenAiLanguageModel.d.ts +250 -57
- package/dist/OpenAiLanguageModel.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.js +311 -160
- package/dist/OpenAiLanguageModel.js.map +1 -1
- package/dist/OpenAiSchema.d.ts +2029 -0
- package/dist/OpenAiSchema.d.ts.map +1 -0
- package/dist/OpenAiSchema.js +591 -0
- package/dist/OpenAiSchema.js.map +1 -0
- package/dist/OpenAiTelemetry.d.ts +31 -18
- package/dist/OpenAiTelemetry.d.ts.map +1 -1
- package/dist/OpenAiTelemetry.js +6 -4
- package/dist/OpenAiTelemetry.js.map +1 -1
- package/dist/OpenAiTool.d.ts +56 -67
- package/dist/OpenAiTool.d.ts.map +1 -1
- package/dist/OpenAiTool.js +33 -44
- package/dist/OpenAiTool.js.map +1 -1
- package/dist/index.d.ts +42 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/errors.js +4 -4
- package/dist/internal/errors.js.map +1 -1
- package/package.json +3 -3
- package/src/Generated.ts +9858 -5044
- package/src/OpenAiClient.ts +396 -90
- package/src/OpenAiClientGenerated.ts +202 -0
- package/src/OpenAiConfig.ts +46 -11
- package/src/OpenAiEmbeddingModel.ts +207 -0
- package/src/OpenAiError.ts +170 -35
- package/src/OpenAiLanguageModel.ts +633 -157
- package/src/OpenAiSchema.ts +984 -0
- package/src/OpenAiTelemetry.ts +32 -19
- package/src/OpenAiTool.ts +34 -45
- package/src/index.ts +45 -8
- package/src/internal/errors.ts +6 -4
package/src/OpenAiClient.ts
CHANGED
|
@@ -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
|
|
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
|
|
20
|
+
import * as Schema from "effect/Schema"
|
|
21
|
+
import * as Scope from "effect/Scope"
|
|
22
|
+
import * as Semaphore from "effect/Semaphore"
|
|
17
23
|
import * as Stream from "effect/Stream"
|
|
18
|
-
import
|
|
24
|
+
import * as AiError from "effect/unstable/ai/AiError"
|
|
25
|
+
import * as ResponseIdTracker from "effect/unstable/ai/ResponseIdTracker"
|
|
19
26
|
import * as Sse from "effect/unstable/encoding/Sse"
|
|
20
27
|
import * as Headers from "effect/unstable/http/Headers"
|
|
21
28
|
import * as HttpBody from "effect/unstable/http/HttpBody"
|
|
22
29
|
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
23
30
|
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
24
|
-
import
|
|
25
|
-
import * as
|
|
31
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
32
|
+
import * as Socket from "effect/unstable/socket/Socket"
|
|
26
33
|
import * as Errors from "./internal/errors.ts"
|
|
27
34
|
import { OpenAiConfig } from "./OpenAiConfig.ts"
|
|
35
|
+
import * as OpenAiSchema from "./OpenAiSchema.ts"
|
|
28
36
|
|
|
29
37
|
// =============================================================================
|
|
30
38
|
// Service Interface
|
|
31
39
|
// =============================================================================
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
|
-
*
|
|
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
|
|
53
|
+
* The transformed HTTP client used by this service.
|
|
42
54
|
*/
|
|
43
|
-
readonly client:
|
|
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
|
|
61
|
+
options: typeof OpenAiSchema.CreateResponse.Encoded
|
|
50
62
|
) => Effect.Effect<
|
|
51
|
-
[body: typeof
|
|
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
|
|
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
|
|
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
|
|
73
|
-
) => Effect.Effect<typeof
|
|
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
|
-
* @
|
|
84
|
-
* @
|
|
95
|
+
* @category services
|
|
96
|
+
* @since 4.0.0
|
|
85
97
|
*/
|
|
86
|
-
export class OpenAiClient extends
|
|
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*(
|
|
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((
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Predicate.isNotUndefined(options.transformClient)
|
|
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
|
|
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
|
|
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
|
|
200
|
+
payload: typeof OpenAiSchema.CreateResponse.Encoded
|
|
189
201
|
): Effect.Effect<
|
|
190
|
-
[body: typeof
|
|
202
|
+
[body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
191
203
|
AiError.AiError
|
|
192
204
|
> =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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(
|
|
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
|
-
)
|
|
245
|
+
)
|
|
221
246
|
return [response, stream]
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
const createResponseStream: Service["createResponseStream"] = (payload) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
239
|
-
): Effect.Effect<typeof
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|