@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.
@@ -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 ServiceMap from "effect/ServiceMap"
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 type * as AiError from "effect/unstable/ai/AiError"
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 type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
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 ServiceMap.Service<OpenAiClient, Service>()(
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*(options: Options): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
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((request) =>
150
- request.pipe(
151
- HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.openai.com/v1"),
152
- Predicate.isNotUndefined(options.apiKey)
153
- ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
154
- : identity,
155
- Predicate.isNotUndefined(options.organizationId)
156
- ? HttpClientRequest.setHeader(
157
- RedactedOpenAiHeaders.OpenAiOrganization,
158
- Redacted.value(options.organizationId)
159
- )
160
- : identity,
161
- Predicate.isNotUndefined(options.projectId)
162
- ? HttpClientRequest.setHeader(
163
- RedactedOpenAiHeaders.OpenAiProject,
164
- Redacted.value(options.projectId)
165
- )
166
- : identity,
167
- HttpClientRequest.acceptJson
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
- httpClientOk.execute(
226
- HttpClientRequest.post("/responses", {
227
- body: HttpBody.jsonUnsafe({ ...payload, stream: true })
228
- })
229
- ).pipe(
230
- Effect.map(buildResponseStream),
231
- Effect.catchTag(
232
- "HttpClientError",
233
- (error) => Errors.mapHttpClientError(error, "createResponseStream")
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>> | undefined
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>> | undefined
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>> | undefined
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)
@@ -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 ServiceMap.Service<
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.services<never>(),
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
+ })