@effect/ai-openai 4.0.0-beta.8 → 4.0.0-beta.80
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 +66675 -37672
- 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 +170 -29
- package/dist/OpenAiClient.d.ts.map +1 -1
- package/dist/OpenAiClient.js +321 -46
- 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 +88 -10
- package/dist/OpenAiConfig.d.ts.map +1 -1
- package/dist/OpenAiConfig.js +42 -7
- package/dist/OpenAiConfig.js.map +1 -1
- package/dist/OpenAiEmbeddingModel.d.ts +188 -0
- package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
- package/dist/OpenAiEmbeddingModel.js +194 -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 +357 -63
- package/dist/OpenAiLanguageModel.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.js +390 -168
- package/dist/OpenAiLanguageModel.js.map +1 -1
- package/dist/OpenAiSchema.d.ts +2325 -0
- package/dist/OpenAiSchema.d.ts.map +1 -0
- package/dist/OpenAiSchema.js +811 -0
- package/dist/OpenAiSchema.js.map +1 -0
- package/dist/OpenAiTelemetry.d.ts +63 -22
- package/dist/OpenAiTelemetry.d.ts.map +1 -1
- package/dist/OpenAiTelemetry.js +20 -10
- package/dist/OpenAiTelemetry.js.map +1 -1
- package/dist/OpenAiTool.d.ts +148 -62
- package/dist/OpenAiTool.d.ts.map +1 -1
- package/dist/OpenAiTool.js +125 -39
- package/dist/OpenAiTool.js.map +1 -1
- package/dist/index.d.ts +19 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -33
- 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 +9717 -4887
- package/src/OpenAiClient.ts +499 -98
- package/src/OpenAiClientGenerated.ts +202 -0
- package/src/OpenAiConfig.ts +89 -11
- package/src/OpenAiEmbeddingModel.ts +332 -0
- package/src/OpenAiError.ts +170 -35
- package/src/OpenAiLanguageModel.ts +776 -169
- package/src/OpenAiSchema.ts +1286 -0
- package/src/OpenAiTelemetry.ts +69 -28
- package/src/OpenAiTool.ts +126 -40
- package/src/index.ts +22 -33
- package/src/internal/errors.ts +6 -4
package/src/OpenAiClient.ts
CHANGED
|
@@ -1,54 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
25
|
-
import * as
|
|
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
|
-
*
|
|
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
|
|
55
|
+
* The transformed HTTP client used by this service.
|
|
42
56
|
*/
|
|
43
|
-
readonly client:
|
|
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
|
|
63
|
+
options: typeof OpenAiSchema.CreateResponse.Encoded
|
|
50
64
|
) => Effect.Effect<
|
|
51
|
-
[body: typeof
|
|
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
|
|
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
|
|
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
|
|
73
|
-
) => Effect.Effect<typeof
|
|
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
|
|
95
|
+
* Service tag for the OpenAI client.
|
|
96
|
+
*
|
|
97
|
+
* **When to use**
|
|
82
98
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
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
|
-
* @
|
|
98
|
-
* @
|
|
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
|
-
*
|
|
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*(
|
|
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((
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
230
|
+
payload: typeof OpenAiSchema.CreateResponse.Encoded
|
|
189
231
|
): Effect.Effect<
|
|
190
|
-
[body: typeof
|
|
232
|
+
[body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
191
233
|
AiError.AiError
|
|
192
234
|
> =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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(
|
|
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
|
-
)
|
|
275
|
+
)
|
|
221
276
|
return [response, stream]
|
|
222
277
|
}
|
|
223
278
|
|
|
224
279
|
const createResponseStream: Service["createResponseStream"] = (payload) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
239
|
-
): Effect.Effect<typeof
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
*
|
|
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
|
|
275
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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)
|