@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.
- 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 +167 -27
- package/dist/OpenAiClient.d.ts.map +1 -1
- package/dist/OpenAiClient.js +337 -44
- 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 +114 -10
- package/dist/OpenAiConfig.d.ts.map +1 -1
- package/dist/OpenAiConfig.js +68 -7
- package/dist/OpenAiConfig.js.map +1 -1
- package/dist/OpenAiEmbeddingModel.d.ts +213 -0
- package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
- package/dist/OpenAiEmbeddingModel.js +219 -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 +384 -62
- package/dist/OpenAiLanguageModel.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.js +416 -166
- package/dist/OpenAiLanguageModel.js.map +1 -1
- package/dist/OpenAiSchema.d.ts +2298 -0
- package/dist/OpenAiSchema.d.ts.map +1 -0
- package/dist/OpenAiSchema.js +814 -0
- package/dist/OpenAiSchema.js.map +1 -0
- package/dist/OpenAiTelemetry.d.ts +59 -18
- package/dist/OpenAiTelemetry.d.ts.map +1 -1
- package/dist/OpenAiTelemetry.js +35 -8
- package/dist/OpenAiTelemetry.js.map +1 -1
- package/dist/OpenAiTool.d.ts +157 -62
- package/dist/OpenAiTool.d.ts.map +1 -1
- package/dist/OpenAiTool.js +134 -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 +9858 -5044
- package/src/OpenAiClient.ts +513 -95
- package/src/OpenAiClientGenerated.ts +202 -0
- package/src/OpenAiConfig.ts +115 -11
- package/src/OpenAiEmbeddingModel.ts +357 -0
- package/src/OpenAiError.ts +170 -35
- package/src/OpenAiLanguageModel.ts +802 -167
- package/src/OpenAiSchema.ts +1289 -0
- package/src/OpenAiTelemetry.ts +81 -23
- package/src/OpenAiTool.ts +135 -40
- package/src/index.ts +22 -33
- package/src/internal/errors.ts +6 -4
package/src/OpenAiClient.ts
CHANGED
|
@@ -1,54 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
25
|
-
import * as
|
|
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
|
-
*
|
|
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
|
|
81
|
+
* The transformed HTTP client used by this service.
|
|
42
82
|
*/
|
|
43
|
-
readonly client:
|
|
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
|
|
89
|
+
options: typeof OpenAiSchema.CreateResponse.Encoded
|
|
50
90
|
) => Effect.Effect<
|
|
51
|
-
[body: typeof
|
|
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
|
|
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
|
|
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
|
|
73
|
-
) => Effect.Effect<typeof
|
|
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
|
-
*
|
|
84
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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*(
|
|
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((
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
257
|
+
payload: typeof OpenAiSchema.CreateResponse.Encoded
|
|
189
258
|
): Effect.Effect<
|
|
190
|
-
[body: typeof
|
|
259
|
+
[body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
191
260
|
AiError.AiError
|
|
192
261
|
> =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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(
|
|
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
|
-
)
|
|
302
|
+
)
|
|
221
303
|
return [response, stream]
|
|
222
304
|
}
|
|
223
305
|
|
|
224
306
|
const createResponseStream: Service["createResponseStream"] = (payload) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
239
|
-
): Effect.Effect<typeof
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
*
|
|
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
|
|
275
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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)
|