import { Effect, Schema } from "effect" import { EmbeddingClient, Service, type Execute } from "./embedding-client.js" import { Media } from "./media.js" import { tryRequest } from "./media-model.js" import { type AIError, type BaseRequest, type HttpOptions, ModelID, ProviderID, ProviderMetadata, ProviderOptions, } from "./schema/index.js" // --------------------------------------------------------------------------- // Input // --------------------------------------------------------------------------- /** One value to embed: text, or a media asset for hosts that embed images, audio or documents. */ export const EmbeddingValue = Schema.Union([Schema.String, Media.AssetSchema]) export type EmbeddingValue = Schema.Schema.Type /** A string is always text; media is an `Asset` or plain bytes or a URL, normalized with `Media.make`. */ export type EmbeddingValueInput = string | Exclude /** * What the vectors are for: a search `query`, or a `document` stored to be searched. Hosts without such a field embed * both the same way and ignore it. */ export type EmbeddingInputType = "query" | "document" // --------------------------------------------------------------------------- // Model // --------------------------------------------------------------------------- export type EmbeddingOptions = Record /** What an embedding model's route does; `EmbeddingRoute.make(...).model(...)` builds one from a protocol. */ export interface EmbeddingModelRoute { readonly id: string generate(request: EmbeddingRequest>, execute: Execute): Effect.Effect } export class EmbeddingModel { declare protected readonly _Options: Options readonly id: ModelID readonly provider: ProviderID readonly route: EmbeddingModelRoute readonly http?: HttpOptions /** Defaults under each request's `providerOptions`, such as options a provider package reads from its settings. */ readonly providerOptions?: ProviderOptions constructor(input: EmbeddingModel.Input) { this.id = input.id this.provider = input.provider this.route = input.route this.http = input.http this.providerOptions = input.providerOptions } static make(input: EmbeddingModel.MakeInput) { return new EmbeddingModel({ ...input, id: ModelID.make(input.id), provider: ProviderID.make(input.provider), }) } } export namespace EmbeddingModel { export interface Input { readonly id: ModelID readonly provider: ProviderID readonly route: EmbeddingModelRoute readonly http?: HttpOptions readonly providerOptions?: ProviderOptions } export interface MakeInput extends Omit, "id" | "provider"> { readonly id: string | ModelID readonly provider: string | ProviderID } } export const EmbeddingModelSchema = Schema.declare( (value): value is EmbeddingModel => value instanceof EmbeddingModel, { expected: "Embedding.Model" }, ) export type EmbeddingModelOptions = Model extends EmbeddingModel ? Options : never // --------------------------------------------------------------------------- // Request and response // --------------------------------------------------------------------------- const EmbeddingRequestSchema = Schema.Struct({ model: EmbeddingModelSchema, values: Schema.Array(EmbeddingValue).pipe( Schema.refine((x): x is typeof x => x.length > 0, { message: "Embedding values must not be empty" }), ), dimensions: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), inputType: Schema.optional(Schema.Literals(["query", "document"])), providerOptions: Schema.optional(ProviderOptions), }) /** A normalized embedding request, the plain object `ai.embedding.request(...)` returns. */ export interface EmbeddingRequest extends BaseRequest> { readonly values: ReadonlyArray /** Requested vector length, for models that can shorten their output. */ readonly dimensions?: number readonly inputType?: EmbeddingInputType } /** Input accepted by `ai.embedding.request` and `generate`. An `EmbeddingRequest` is also a valid input. */ export interface EmbeddingRequestInput extends BaseRequest>> { readonly values: ReadonlyArray readonly dimensions?: number readonly inputType?: EmbeddingInputType } /** What embedding hosts report; their raw usage stays in the response's `providerMetadata`. */ export class EmbeddingUsage extends Schema.Class("Embedding.Usage")({ inputTokens: Schema.optional(Schema.Number), }) {} export class EmbeddingResponse extends Schema.Class("Embedding.Response")({ /** One vector per value, in value order. */ embeddings: Schema.Array(Schema.Array(Schema.Number)), usage: Schema.optional(EmbeddingUsage), providerMetadata: Schema.optional(ProviderMetadata), }) {} // --------------------------------------------------------------------------- // Call API // --------------------------------------------------------------------------- /** Normalize embedding request input into a plain `EmbeddingRequest`; non-string values become `Media.Asset`s. */ export const request = ( input: EmbeddingRequestInput, ): EmbeddingRequest => ({ ...input, values: input.values.map((value) => (typeof value === "string" ? value : Media.make(value))), }) const validate = Schema.decodeUnknownSync(EmbeddingRequestSchema) export function generate( input: EmbeddingRequestInput, ): Effect.Effect { return tryRequest(() => { const normalized = request(input) validate(normalized) return normalized }).pipe(Effect.flatMap(EmbeddingClient.generate)) } export const Embedding = { request, generate, } as const