@effect/ai-openai 4.0.0-beta.5 → 4.0.0-beta.50
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 +4060 -4038
- 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 +58 -12
- package/dist/OpenAiClient.d.ts.map +1 -1
- package/dist/OpenAiClient.js +191 -9
- package/dist/OpenAiClient.js.map +1 -1
- package/dist/OpenAiConfig.d.ts +2 -2
- package/dist/OpenAiConfig.d.ts.map +1 -1
- package/dist/OpenAiConfig.js +3 -3
- package/dist/OpenAiConfig.js.map +1 -1
- package/dist/OpenAiEmbeddingModel.d.ts +85 -0
- package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
- package/dist/OpenAiEmbeddingModel.js +116 -0
- package/dist/OpenAiEmbeddingModel.js.map +1 -0
- package/dist/OpenAiError.d.ts +22 -32
- package/dist/OpenAiError.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.d.ts +10 -2
- package/dist/OpenAiLanguageModel.d.ts.map +1 -1
- package/dist/OpenAiLanguageModel.js +187 -68
- package/dist/OpenAiLanguageModel.js.map +1 -1
- package/dist/OpenAiTool.d.ts +24 -24
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- 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 +125 -122
- package/src/OpenAiClient.ts +307 -42
- package/src/OpenAiConfig.ts +3 -3
- package/src/OpenAiEmbeddingModel.ts +200 -0
- package/src/OpenAiError.ts +24 -32
- package/src/OpenAiLanguageModel.ts +228 -70
- package/src/index.ts +9 -0
- package/src/internal/errors.ts +6 -4
package/src/OpenAiClient.ts
CHANGED
|
@@ -8,20 +8,28 @@
|
|
|
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
|
|
31
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
32
|
+
import * as Socket from "effect/unstable/socket/Socket"
|
|
25
33
|
import * as Generated from "./Generated.ts"
|
|
26
34
|
import * as Errors from "./internal/errors.ts"
|
|
27
35
|
import { OpenAiConfig } from "./OpenAiConfig.ts"
|
|
@@ -48,7 +56,7 @@ export interface Service {
|
|
|
48
56
|
readonly createResponse: (
|
|
49
57
|
options: typeof Generated.CreateResponse.Encoded
|
|
50
58
|
) => Effect.Effect<
|
|
51
|
-
[body: typeof Generated.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
59
|
+
readonly [body: typeof Generated.Response.Type, response: HttpClientResponse.HttpClientResponse],
|
|
52
60
|
AiError.AiError
|
|
53
61
|
>
|
|
54
62
|
|
|
@@ -58,7 +66,7 @@ export interface Service {
|
|
|
58
66
|
readonly createResponseStream: (
|
|
59
67
|
options: Omit<typeof Generated.CreateResponse.Encoded, "stream">
|
|
60
68
|
) => Effect.Effect<
|
|
61
|
-
[
|
|
69
|
+
readonly [
|
|
62
70
|
response: HttpClientResponse.HttpClientResponse,
|
|
63
71
|
stream: Stream.Stream<typeof Generated.ResponseStreamEvent.Type, AiError.AiError>
|
|
64
72
|
],
|
|
@@ -83,7 +91,7 @@ export interface Service {
|
|
|
83
91
|
* @since 1.0.0
|
|
84
92
|
* @category service
|
|
85
93
|
*/
|
|
86
|
-
export class OpenAiClient extends
|
|
94
|
+
export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
|
|
87
95
|
"@effect/ai-openai/OpenAiClient"
|
|
88
96
|
) {}
|
|
89
97
|
|
|
@@ -142,32 +150,33 @@ const RedactedOpenAiHeaders = {
|
|
|
142
150
|
* @category constructors
|
|
143
151
|
*/
|
|
144
152
|
export const make = Effect.fnUntraced(
|
|
145
|
-
function*(
|
|
153
|
+
function*(
|
|
154
|
+
options: Options
|
|
155
|
+
): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
|
|
146
156
|
const baseClient = yield* HttpClient.HttpClient
|
|
157
|
+
const apiUrl = options.apiUrl ?? "https://api.openai.com/v1"
|
|
147
158
|
|
|
148
159
|
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)
|
|
160
|
+
HttpClient.mapRequest(Function.flow(
|
|
161
|
+
HttpClientRequest.prependUrl(apiUrl),
|
|
162
|
+
options.apiKey
|
|
163
|
+
? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
|
|
164
|
+
: identity,
|
|
165
|
+
options.organizationId
|
|
166
|
+
? HttpClientRequest.setHeader(
|
|
167
|
+
RedactedOpenAiHeaders.OpenAiOrganization,
|
|
168
|
+
Redacted.value(options.organizationId)
|
|
169
|
+
)
|
|
170
|
+
: identity,
|
|
171
|
+
options.projectId
|
|
172
|
+
? HttpClientRequest.setHeader(
|
|
173
|
+
RedactedOpenAiHeaders.OpenAiProject,
|
|
174
|
+
Redacted.value(options.projectId)
|
|
175
|
+
)
|
|
176
|
+
: identity,
|
|
177
|
+
HttpClientRequest.acceptJson
|
|
178
|
+
)),
|
|
179
|
+
options.transformClient
|
|
171
180
|
? options.transformClient
|
|
172
181
|
: identity
|
|
173
182
|
)
|
|
@@ -222,17 +231,21 @@ export const make = Effect.fnUntraced(
|
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
const createResponseStream: Service["createResponseStream"] = (payload) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
(
|
|
234
|
+
Effect.contextWith((services) => {
|
|
235
|
+
const socket = Context.getOrUndefined(services, OpenAiSocket)
|
|
236
|
+
if (socket) return socket.createResponseStream(payload)
|
|
237
|
+
return httpClientOk.execute(
|
|
238
|
+
HttpClientRequest.post("/responses", {
|
|
239
|
+
body: HttpBody.jsonUnsafe({ ...payload, stream: true })
|
|
240
|
+
})
|
|
241
|
+
).pipe(
|
|
242
|
+
Effect.map(buildResponseStream),
|
|
243
|
+
Effect.catchTag(
|
|
244
|
+
"HttpClientError",
|
|
245
|
+
(error) => Errors.mapHttpClientError(error, "createResponseStream")
|
|
246
|
+
)
|
|
234
247
|
)
|
|
235
|
-
)
|
|
248
|
+
})
|
|
236
249
|
|
|
237
250
|
const createEmbedding = (
|
|
238
251
|
payload: typeof Generated.CreateEmbeddingRequest.Encoded
|
|
@@ -281,7 +294,7 @@ export const layerConfig = (options?: {
|
|
|
281
294
|
/**
|
|
282
295
|
* The config value to load for the API key.
|
|
283
296
|
*/
|
|
284
|
-
readonly apiKey?: Config.Config<Redacted.Redacted<string
|
|
297
|
+
readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
285
298
|
|
|
286
299
|
/**
|
|
287
300
|
* The config value to load for the API URL.
|
|
@@ -291,12 +304,12 @@ export const layerConfig = (options?: {
|
|
|
291
304
|
/**
|
|
292
305
|
* The config value to load for the organization ID.
|
|
293
306
|
*/
|
|
294
|
-
readonly organizationId?: Config.Config<Redacted.Redacted<string
|
|
307
|
+
readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
295
308
|
|
|
296
309
|
/**
|
|
297
310
|
* The config value to load for the project ID.
|
|
298
311
|
*/
|
|
299
|
-
readonly projectId?: Config.Config<Redacted.Redacted<string
|
|
312
|
+
readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
|
|
300
313
|
|
|
301
314
|
/**
|
|
302
315
|
* Optional transformer for the HTTP client.
|
|
@@ -327,3 +340,255 @@ export const layerConfig = (options?: {
|
|
|
327
340
|
})
|
|
328
341
|
})
|
|
329
342
|
)
|
|
343
|
+
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// Websocket mode
|
|
346
|
+
// =============================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @since 1.0.0
|
|
350
|
+
* @category Events
|
|
351
|
+
*/
|
|
352
|
+
export type ResponseStreamEvent = typeof Generated.ResponseStreamEvent.Type
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @since 1.0.0
|
|
356
|
+
* @category Websocket mode
|
|
357
|
+
*/
|
|
358
|
+
export class OpenAiSocket extends Context.Service<OpenAiSocket, {
|
|
359
|
+
/**
|
|
360
|
+
* Create a streaming response using the OpenAI responses endpoint.
|
|
361
|
+
*/
|
|
362
|
+
readonly createResponseStream: (
|
|
363
|
+
options: Omit<typeof Generated.CreateResponse.Encoded, "stream">
|
|
364
|
+
) => Effect.Effect<
|
|
365
|
+
readonly [
|
|
366
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
367
|
+
stream: Stream.Stream<ResponseStreamEvent, AiError.AiError>
|
|
368
|
+
],
|
|
369
|
+
AiError.AiError
|
|
370
|
+
>
|
|
371
|
+
}>()("@effect/ai-openai/OpenAiClient/OpenAiSocket") {}
|
|
372
|
+
|
|
373
|
+
const makeSocket = Effect.gen(function*() {
|
|
374
|
+
const client = yield* OpenAiClient
|
|
375
|
+
const tracker = yield* ResponseIdTracker.make
|
|
376
|
+
const socketScope = yield* Effect.scope
|
|
377
|
+
const makeRequest = Effect.orDie(client.client.httpClient.preprocess(HttpClientRequest.post("/responses")))
|
|
378
|
+
const makeWebSocket = yield* Socket.WebSocketConstructor
|
|
379
|
+
|
|
380
|
+
const decoder = new TextDecoder()
|
|
381
|
+
|
|
382
|
+
const queueRef: RcRef.RcRef<
|
|
383
|
+
{
|
|
384
|
+
readonly send: (message: typeof Generated.CreateResponse.Encoded) => Effect.Effect<void, AiError.AiError>
|
|
385
|
+
readonly incoming: Queue.Dequeue<ResponseStreamEvent, AiError.AiError>
|
|
386
|
+
}
|
|
387
|
+
> = yield* RcRef.make({
|
|
388
|
+
idleTimeToLive: 60_000,
|
|
389
|
+
acquire: Effect.gen(function*() {
|
|
390
|
+
const scope = yield* Effect.scope
|
|
391
|
+
const request = yield* makeRequest
|
|
392
|
+
const socket = yield* Socket.makeWebSocket(request.url.replace(/^http/, "ws")).pipe(
|
|
393
|
+
Effect.provideService(Socket.WebSocketConstructor, (url) =>
|
|
394
|
+
makeWebSocket(url, {
|
|
395
|
+
headers: request.headers
|
|
396
|
+
} as any))
|
|
397
|
+
)
|
|
398
|
+
const write = yield* socket.writer
|
|
399
|
+
|
|
400
|
+
yield* Scope.addFinalizerExit(scope, () => {
|
|
401
|
+
tracker.clearUnsafe()
|
|
402
|
+
return Effect.void
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const incoming = yield* Queue.unbounded<ResponseStreamEvent, AiError.AiError>()
|
|
406
|
+
const send = (message: typeof Generated.CreateResponse.Encoded) =>
|
|
407
|
+
write(JSON.stringify({
|
|
408
|
+
type: "response.create",
|
|
409
|
+
...message
|
|
410
|
+
})).pipe(
|
|
411
|
+
Effect.mapError((_error) =>
|
|
412
|
+
AiError.make({
|
|
413
|
+
module: "OpenAiClient",
|
|
414
|
+
method: "createResponseStream",
|
|
415
|
+
reason: new AiError.NetworkError({
|
|
416
|
+
reason: "TransportError",
|
|
417
|
+
request: {
|
|
418
|
+
method: "POST",
|
|
419
|
+
url: request.url,
|
|
420
|
+
urlParams: [],
|
|
421
|
+
hash: undefined,
|
|
422
|
+
headers: request.headers
|
|
423
|
+
},
|
|
424
|
+
description: "Failed to send message over WebSocket"
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
yield* socket.runRaw((msg) => {
|
|
431
|
+
const text = typeof msg === "string" ? msg : decoder.decode(msg)
|
|
432
|
+
try {
|
|
433
|
+
const event = decodeEvent(text)
|
|
434
|
+
if (event.type === "error" && "status" in event) {
|
|
435
|
+
const json = JSON.stringify(event.error)
|
|
436
|
+
return Effect.fail(
|
|
437
|
+
AiError.make({
|
|
438
|
+
module: "OpenAiClient",
|
|
439
|
+
method: "createResponseStream",
|
|
440
|
+
reason: AiError.reasonFromHttpStatus({
|
|
441
|
+
description: json,
|
|
442
|
+
status: event.status,
|
|
443
|
+
metadata: event.error,
|
|
444
|
+
http: {
|
|
445
|
+
body: json,
|
|
446
|
+
request: {
|
|
447
|
+
method: "POST",
|
|
448
|
+
url: request.url,
|
|
449
|
+
urlParams: [],
|
|
450
|
+
hash: undefined,
|
|
451
|
+
headers: request.headers
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
Queue.offerUnsafe(incoming, event)
|
|
459
|
+
} catch {}
|
|
460
|
+
}).pipe(
|
|
461
|
+
Effect.catchTag("SocketError", (error) =>
|
|
462
|
+
AiError.make({
|
|
463
|
+
module: "OpenAiClient",
|
|
464
|
+
method: "createResponseStream",
|
|
465
|
+
reason: new AiError.NetworkError({
|
|
466
|
+
reason: "TransportError",
|
|
467
|
+
request: {
|
|
468
|
+
method: "POST",
|
|
469
|
+
url: request.url,
|
|
470
|
+
urlParams: [],
|
|
471
|
+
hash: undefined,
|
|
472
|
+
headers: request.headers
|
|
473
|
+
},
|
|
474
|
+
description: error.message
|
|
475
|
+
})
|
|
476
|
+
}).asEffect()),
|
|
477
|
+
Effect.catchCause((cause) => Queue.failCause(incoming, cause)),
|
|
478
|
+
Effect.ensuring(Effect.forkIn(RcRef.invalidate(queueRef), socketScope, {
|
|
479
|
+
startImmediately: true
|
|
480
|
+
})),
|
|
481
|
+
Effect.forkScoped({ startImmediately: true })
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return { send, incoming } as const
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
// Prime the websocket
|
|
489
|
+
yield* Effect.scoped(RcRef.get(queueRef))
|
|
490
|
+
|
|
491
|
+
// Websocket mode only allows one request at a time
|
|
492
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
493
|
+
const request = yield* makeRequest
|
|
494
|
+
|
|
495
|
+
return OpenAiSocket.context({
|
|
496
|
+
createResponseStream(options) {
|
|
497
|
+
const stream = Stream.unwrap(Effect.gen(function*() {
|
|
498
|
+
const scope = yield* Effect.scope
|
|
499
|
+
yield* Effect.acquireRelease(
|
|
500
|
+
semaphore.take(1),
|
|
501
|
+
() => semaphore.release(1),
|
|
502
|
+
{ interruptible: true }
|
|
503
|
+
)
|
|
504
|
+
const { send, incoming } = yield* RcRef.get(queueRef)
|
|
505
|
+
let done = false
|
|
506
|
+
|
|
507
|
+
yield* Scope.addFinalizerExit(
|
|
508
|
+
scope,
|
|
509
|
+
() => done ? Effect.void : RcRef.invalidate(queueRef)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
yield* send(options).pipe(
|
|
513
|
+
Effect.forkScoped({ startImmediately: true })
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return Stream.fromQueue(incoming).pipe(
|
|
517
|
+
Stream.takeUntil((e) => {
|
|
518
|
+
done = e.type === "response.completed" || e.type === "response.incomplete"
|
|
519
|
+
return done
|
|
520
|
+
})
|
|
521
|
+
)
|
|
522
|
+
}))
|
|
523
|
+
|
|
524
|
+
return Effect.succeed([
|
|
525
|
+
HttpClientResponse.fromWeb(request, new Response()),
|
|
526
|
+
stream
|
|
527
|
+
])
|
|
528
|
+
}
|
|
529
|
+
}).pipe(
|
|
530
|
+
Context.add(ResponseIdTracker.ResponseIdTracker, tracker)
|
|
531
|
+
)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const ErrorEvent = Schema.Struct({
|
|
535
|
+
type: Schema.Literal("error"),
|
|
536
|
+
status: Schema.Number.pipe(
|
|
537
|
+
Schema.withDecodingDefault(Effect.succeed(500))
|
|
538
|
+
),
|
|
539
|
+
error: Schema.Struct({
|
|
540
|
+
type: Schema.String,
|
|
541
|
+
message: Schema.String
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
const AllEvents = Schema.Union([ErrorEvent, Generated.ResponseStreamEvent])
|
|
546
|
+
const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents))
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Uses OpenAI's websocket mode for all responses within the provided effect.
|
|
550
|
+
*
|
|
551
|
+
* Note: This only works with the following WebSocket constructor layers:
|
|
552
|
+
*
|
|
553
|
+
* - `NodeSocket.layerWebSocketConstructorWS`
|
|
554
|
+
* - `BunSocket.layerWebSocketConstructor`
|
|
555
|
+
*
|
|
556
|
+
* This is because it needs to use non-standard options for setting the
|
|
557
|
+
* Authorization header.
|
|
558
|
+
*
|
|
559
|
+
* @since 1.0.0
|
|
560
|
+
* @category Websocket mode
|
|
561
|
+
*/
|
|
562
|
+
export const withWebSocketMode = <A, E, R>(
|
|
563
|
+
effect: Effect.Effect<A, E, R>
|
|
564
|
+
): Effect.Effect<
|
|
565
|
+
A,
|
|
566
|
+
E,
|
|
567
|
+
Exclude<R, OpenAiSocket | ResponseIdTracker.ResponseIdTracker> | OpenAiClient | Socket.WebSocketConstructor
|
|
568
|
+
> =>
|
|
569
|
+
Effect.scopedWith((scope) =>
|
|
570
|
+
Effect.flatMap(
|
|
571
|
+
Scope.provide(makeSocket, scope),
|
|
572
|
+
(services) => Effect.provideContext(effect, services)
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Uses OpenAI's websocket mode for all responses that use the Layer.
|
|
578
|
+
*
|
|
579
|
+
* Note: This only works with the following WebSocket constructor layers:
|
|
580
|
+
*
|
|
581
|
+
* - `NodeSocket.layerWebSocketConstructorWS`
|
|
582
|
+
* - `BunSocket.layerWebSocketConstructor`
|
|
583
|
+
*
|
|
584
|
+
* This is because it needs to use non-standard options for setting the
|
|
585
|
+
* Authorization header.
|
|
586
|
+
*
|
|
587
|
+
* @since 1.0.0
|
|
588
|
+
* @category Websocket mode
|
|
589
|
+
*/
|
|
590
|
+
export const layerWebSocketMode: Layer.Layer<
|
|
591
|
+
OpenAiSocket | ResponseIdTracker.ResponseIdTracker,
|
|
592
|
+
never,
|
|
593
|
+
OpenAiClient | Socket.WebSocketConstructor
|
|
594
|
+
> = Layer.effectContext(makeSocket)
|
package/src/OpenAiConfig.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
|
+
import * as Context from "effect/Context"
|
|
4
5
|
import * as Effect from "effect/Effect"
|
|
5
6
|
import { dual } from "effect/Function"
|
|
6
|
-
import * as ServiceMap from "effect/ServiceMap"
|
|
7
7
|
import type { HttpClient } from "effect/unstable/http/HttpClient"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @since 1.0.0
|
|
11
11
|
* @category services
|
|
12
12
|
*/
|
|
13
|
-
export class OpenAiConfig extends
|
|
13
|
+
export class OpenAiConfig extends Context.Service<
|
|
14
14
|
OpenAiConfig,
|
|
15
15
|
OpenAiConfig.Service
|
|
16
16
|
>()("@effect/ai-openai/OpenAiConfig") {
|
|
@@ -18,7 +18,7 @@ export class OpenAiConfig extends ServiceMap.Service<
|
|
|
18
18
|
* @since 1.0.0
|
|
19
19
|
*/
|
|
20
20
|
static readonly getOrUndefined: Effect.Effect<typeof OpenAiConfig.Service | undefined> = Effect.map(
|
|
21
|
-
Effect.
|
|
21
|
+
Effect.context<never>(),
|
|
22
22
|
(context) => context.mapUnsafe.get(OpenAiConfig.key)
|
|
23
23
|
)
|
|
24
24
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Embedding Model implementation.
|
|
3
|
+
*
|
|
4
|
+
* Provides an EmbeddingModel implementation for OpenAI's embeddings API.
|
|
5
|
+
*
|
|
6
|
+
* @since 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
import * as Context from "effect/Context"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import { dual } from "effect/Function"
|
|
11
|
+
import * as Layer from "effect/Layer"
|
|
12
|
+
import type { Simplify } from "effect/Types"
|
|
13
|
+
import * as AiError from "effect/unstable/ai/AiError"
|
|
14
|
+
import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel"
|
|
15
|
+
import * as AiModel from "effect/unstable/ai/Model"
|
|
16
|
+
import type * as Generated from "./Generated.ts"
|
|
17
|
+
import { OpenAiClient } from "./OpenAiClient.ts"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
* @category models
|
|
22
|
+
*/
|
|
23
|
+
export type Model = "text-embedding-ada-002" | "text-embedding-3-small" | "text-embedding-3-large"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Service definition for OpenAI embedding model configuration.
|
|
27
|
+
*
|
|
28
|
+
* @since 1.0.0
|
|
29
|
+
* @category services
|
|
30
|
+
*/
|
|
31
|
+
export class Config extends Context.Service<
|
|
32
|
+
Config,
|
|
33
|
+
Simplify<
|
|
34
|
+
& Partial<
|
|
35
|
+
Omit<
|
|
36
|
+
typeof Generated.CreateEmbeddingRequest.Encoded,
|
|
37
|
+
"input"
|
|
38
|
+
>
|
|
39
|
+
>
|
|
40
|
+
& {
|
|
41
|
+
readonly [x: string]: unknown
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
>()("@effect/ai-openai/OpenAiEmbeddingModel/Config") {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
* @category constructors
|
|
49
|
+
*/
|
|
50
|
+
export const model = (
|
|
51
|
+
model: (string & {}) | Model,
|
|
52
|
+
options: {
|
|
53
|
+
readonly dimensions: number
|
|
54
|
+
readonly config?: Omit<typeof Config.Service, "model" | "dimensions">
|
|
55
|
+
}
|
|
56
|
+
): AiModel.Model<"openai", EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient> =>
|
|
57
|
+
AiModel.make(
|
|
58
|
+
"openai",
|
|
59
|
+
model,
|
|
60
|
+
Layer.merge(
|
|
61
|
+
layer({
|
|
62
|
+
model,
|
|
63
|
+
config: {
|
|
64
|
+
...options.config,
|
|
65
|
+
dimensions: options.dimensions
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
Layer.succeed(EmbeddingModel.Dimensions, options.dimensions)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates an OpenAI embedding model service.
|
|
74
|
+
*
|
|
75
|
+
* @since 1.0.0
|
|
76
|
+
* @category constructors
|
|
77
|
+
*/
|
|
78
|
+
export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
|
|
79
|
+
readonly model: (string & {}) | Model
|
|
80
|
+
readonly config?: Omit<typeof Config.Service, "model"> | undefined
|
|
81
|
+
}): Effect.fn.Return<EmbeddingModel.Service, never, OpenAiClient> {
|
|
82
|
+
const client = yield* OpenAiClient
|
|
83
|
+
|
|
84
|
+
const makeConfig = Effect.gen(function*() {
|
|
85
|
+
const services = yield* Effect.context<never>()
|
|
86
|
+
return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return yield* EmbeddingModel.make({
|
|
90
|
+
embedMany: Effect.fnUntraced(function*({ inputs }) {
|
|
91
|
+
const config = yield* makeConfig
|
|
92
|
+
const response = yield* client.createEmbedding({ ...config, input: inputs })
|
|
93
|
+
return yield* mapProviderResponse(inputs.length, response)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates a layer for the OpenAI embedding model.
|
|
100
|
+
*
|
|
101
|
+
* @since 1.0.0
|
|
102
|
+
* @category layers
|
|
103
|
+
*/
|
|
104
|
+
export const layer = (options: {
|
|
105
|
+
readonly model: (string & {}) | Model
|
|
106
|
+
readonly config?: Omit<typeof Config.Service, "model"> | undefined
|
|
107
|
+
}): Layer.Layer<EmbeddingModel.EmbeddingModel, never, OpenAiClient> =>
|
|
108
|
+
Layer.effect(EmbeddingModel.EmbeddingModel, make(options))
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Provides config overrides for OpenAI embedding model operations.
|
|
112
|
+
*
|
|
113
|
+
* @since 1.0.0
|
|
114
|
+
* @category configuration
|
|
115
|
+
*/
|
|
116
|
+
export const withConfigOverride: {
|
|
117
|
+
/**
|
|
118
|
+
* Provides config overrides for OpenAI embedding model operations.
|
|
119
|
+
*
|
|
120
|
+
* @since 1.0.0
|
|
121
|
+
* @category configuration
|
|
122
|
+
*/
|
|
123
|
+
(overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
124
|
+
/**
|
|
125
|
+
* Provides config overrides for OpenAI embedding model operations.
|
|
126
|
+
*
|
|
127
|
+
* @since 1.0.0
|
|
128
|
+
* @category configuration
|
|
129
|
+
*/
|
|
130
|
+
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
|
|
131
|
+
} = dual<
|
|
132
|
+
/**
|
|
133
|
+
* Provides config overrides for OpenAI embedding model operations.
|
|
134
|
+
*
|
|
135
|
+
* @since 1.0.0
|
|
136
|
+
* @category configuration
|
|
137
|
+
*/
|
|
138
|
+
(overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
|
|
139
|
+
/**
|
|
140
|
+
* Provides config overrides for OpenAI embedding model operations.
|
|
141
|
+
*
|
|
142
|
+
* @since 1.0.0
|
|
143
|
+
* @category configuration
|
|
144
|
+
*/
|
|
145
|
+
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
146
|
+
>(2, (self, overrides) =>
|
|
147
|
+
Effect.flatMap(
|
|
148
|
+
Effect.serviceOption(Config),
|
|
149
|
+
(config) =>
|
|
150
|
+
Effect.provideService(self, Config, {
|
|
151
|
+
...(config._tag === "Some" ? config.value : {}),
|
|
152
|
+
...overrides
|
|
153
|
+
})
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
const mapProviderResponse = (
|
|
157
|
+
inputLength: number,
|
|
158
|
+
response: typeof Generated.CreateEmbeddingResponse.Type
|
|
159
|
+
): Effect.Effect<EmbeddingModel.ProviderResponse, AiError.AiError> => {
|
|
160
|
+
if (response.data.length !== inputLength) {
|
|
161
|
+
return Effect.fail(
|
|
162
|
+
invalidOutput("Provider returned " + response.data.length + " embeddings but expected " + inputLength)
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const results = new Array<Array<number>>(inputLength)
|
|
167
|
+
const seen = new Set<number>()
|
|
168
|
+
|
|
169
|
+
for (const entry of response.data) {
|
|
170
|
+
if (!Number.isInteger(entry.index) || entry.index < 0 || entry.index >= inputLength) {
|
|
171
|
+
return Effect.fail(invalidOutput("Provider returned invalid embedding index: " + entry.index))
|
|
172
|
+
}
|
|
173
|
+
if (seen.has(entry.index)) {
|
|
174
|
+
return Effect.fail(invalidOutput("Provider returned duplicate embedding index: " + entry.index))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
seen.add(entry.index)
|
|
178
|
+
results[entry.index] = [...entry.embedding]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (seen.size !== inputLength) {
|
|
182
|
+
return Effect.fail(
|
|
183
|
+
invalidOutput("Provider returned embeddings for " + seen.size + " inputs but expected " + inputLength)
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Effect.succeed({
|
|
188
|
+
results,
|
|
189
|
+
usage: {
|
|
190
|
+
inputTokens: response.usage?.prompt_tokens
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const invalidOutput = (description: string): AiError.AiError =>
|
|
196
|
+
AiError.make({
|
|
197
|
+
module: "OpenAiEmbeddingModel",
|
|
198
|
+
method: "embedMany",
|
|
199
|
+
reason: new AiError.InvalidOutputError({ description })
|
|
200
|
+
})
|