import { Effect } from "effect" import { Endpoint } from "./route/endpoint.js" import { MediaRoute } from "./route/media.js" import type { MediaProtocol } from "./route/media-protocol.js" import { AIError, InvalidRequestError, ModelID, ProviderID, type HttpOptions, type ProviderOptions, } from "./schema/index.js" /** * What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes * (`ImageModel`, `VideoModel`, `SpeechModel`, `TranscriptionModel`) extend it with their route type and a nominal * marker so one cannot stand in for the other in requests. */ export class MediaModel { // As with `LanguageModel`, the route type is erased over `Options`; `fromRoute` and the constructor trust that the // route accepts every request this model's `Options` admit. declare protected readonly _Options: Options readonly id: ModelID readonly provider: ProviderID readonly route: Route readonly http?: HttpOptions /** Defaults under each request's `providerOptions`, such as options a provider package reads from its settings. */ readonly providerOptions?: ProviderOptions constructor(input: MediaModel.Input) { this.id = ModelID.make(input.id) this.provider = ProviderID.make(input.provider) this.route = input.route this.http = input.http this.providerOptions = input.providerOptions } } export namespace MediaModel { export type Options = ProviderOptions export interface Input { readonly id: string | ModelID readonly provider: string | ProviderID readonly route: Route readonly http?: HttpOptions readonly providerOptions?: ProviderOptions } /** A protocol plus its canonical start path; `ModelInput.baseURL` overrides `baseURL` per deployment. */ export interface RouteInput { readonly protocol: Protocol readonly path: Endpoint.EndpointPart readonly baseURL?: string /** Headers the protocol requires on every call, such as a pinned API version; deployment headers win. */ readonly headers?: Record } export type InlineRouteInput = RouteInput< Request, MediaProtocol.Inline > export type StreamRouteInput = RouteInput< MediaProtocol.Addressed, MediaProtocol.Streamed > export type QueuedRouteInput = RouteInput< Request, MediaProtocol.Queued > export type AnyRouteInput = | InlineRouteInput | StreamRouteInput | QueuedRouteInput } /** Compose a protocol route input with one deployment through `MediaRoute.inline`, `queued`, or `stream`. */ export const composeRoute = ( route: MediaModel.AnyRouteInput, input: MediaRoute.ModelInput, collect: (events: ReadonlyArray) => Effect.Effect, ): MediaRoute.AnyRoute => { if (isStreamInput(route)) return MediaRoute.stream({ ...composition(route, input), collect }) if (isQueuedInput(route)) return MediaRoute.queued(composition(route, input)) return MediaRoute.inline(composition(route, input)) } const composition = ( route: MediaModel.RouteInput, input: MediaRoute.ModelInput, ): MediaRoute.Composition & { readonly protocol: Protocol } => ({ protocol: route.protocol, endpoint: Endpoint.path(route.path, { baseURL: input.baseURL ?? route.baseURL }), auth: input.auth, headers: route.headers === undefined && input.headers === undefined ? undefined : { ...route.headers, ...input.headers }, }) const isStreamInput = ( route: MediaModel.AnyRouteInput, ): route is MediaModel.StreamRouteInput => route.protocol.kind === "stream" const isQueuedInput = ( route: MediaModel.AnyRouteInput, ): route is MediaModel.QueuedRouteInput => route.protocol.kind === "queued" /** Lift a synchronous Schema-class constructor into a typed `InvalidRequest` failure. */ export const tryRequest = (make: () => A): Effect.Effect => Effect.try({ try: make, catch: (error) => error instanceof AIError ? error : new AIError({ reason: new InvalidRequestError({ message: error instanceof Error ? error.message : String(error), cause: error, }), }), })