| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- export * as AISDK from "./aisdk"
- import { makeLocationNode } from "./effect/app-node"
- import type { LanguageModelV3 } from "@ai-sdk/provider"
- import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
- import { ModelV2 } from "./model"
- import { ProviderV2 } from "./provider"
- import { State } from "./state"
- type SDK = any
- export interface SDKEvent {
- readonly model: ModelV2.Info
- readonly package: string
- readonly options: Record<string, any>
- sdk?: SDK
- }
- export interface LanguageEvent {
- readonly model: ModelV2.Info
- readonly sdk: SDK
- readonly options: Record<string, any>
- language?: LanguageModelV3
- }
- function wrapSSE(res: Response, ms: number, ctl: AbortController) {
- if (typeof ms !== "number" || ms <= 0) return res
- if (!res.body) return res
- if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
- const reader = res.body.getReader()
- const body = new ReadableStream<Uint8Array>({
- async pull(ctrl) {
- const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
- const id = setTimeout(() => {
- const err = new Error("SSE read timed out")
- ctl.abort(err)
- void reader.cancel(err)
- reject(err)
- }, ms)
- reader.read().then(
- (part) => {
- clearTimeout(id)
- resolve(part)
- },
- (err) => {
- clearTimeout(id)
- reject(err)
- },
- )
- })
- if (part.done) {
- ctrl.close()
- return
- }
- ctrl.enqueue(part.value)
- },
- async cancel(reason) {
- ctl.abort(reason)
- await reader.cancel(reason)
- },
- })
- return new Response(body, {
- headers: new Headers(res.headers),
- status: res.status,
- statusText: res.statusText,
- })
- }
- function prepareOptions(model: ModelV2.Info, pkg: string) {
- const options: Record<string, any> = {
- name: model.providerID,
- ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
- ...model.request.body,
- }
- if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
- const customFetch = options.fetch
- const chunkTimeout = options.chunkTimeout
- delete options.chunkTimeout
- options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
- const opts = { ...(init ?? {}) }
- const signals = [
- opts.signal,
- typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined,
- options.timeout !== undefined && options.timeout !== null && options.timeout !== false
- ? AbortSignal.timeout(options.timeout)
- : undefined,
- ].filter((item): item is AbortSignal | AbortController => Boolean(item))
- const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController)
- const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item))
- if (abortSignals.length === 1) opts.signal = abortSignals[0]
- if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
- if (
- (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") &&
- opts.body &&
- opts.method === "POST"
- ) {
- const body = JSON.parse(opts.body as string)
- if (body.store !== true && Array.isArray(body.input)) {
- for (const item of body.input) {
- if ("id" in item) delete item.id
- }
- opts.body = JSON.stringify(body)
- }
- }
- const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
- ...opts,
- timeout: false,
- })
- if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
- return wrapSSE(res, chunkTimeout, chunkAbortCtl)
- }
- return options
- }
- export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
- providerID: ProviderV2.ID,
- cause: Schema.Defect(),
- }) {}
- function initError(providerID: ProviderV2.ID) {
- return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
- }
- export interface Interface {
- readonly hook: {
- readonly sdk: (
- callback: (event: SDKEvent) => Effect.Effect<void> | void,
- ) => Effect.Effect<State.Registration, never, Scope.Scope>
- readonly language: (
- callback: (event: LanguageEvent) => Effect.Effect<void> | void,
- ) => Effect.Effect<State.Registration, never, Scope.Scope>
- }
- readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
- readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
- readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
- }
- export class Service extends Context.Service<Service, Interface>()("@kirincode/v2/AISDK") {}
- export const locationLayer = Layer.effect(
- Service,
- Effect.gen(function* () {
- let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
- let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
- const languages = new Map<string, LanguageModelV3>()
- const sdks = new Map<string, SDK>()
- const register = <Event>(
- hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
- update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
- ) =>
- Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
- const scope = yield* Scope.Scope
- let active = true
- update([...hooks(), callback])
- const dispose = Effect.sync(() => {
- if (!active) return
- active = false
- update(hooks().filter((item) => item !== callback))
- })
- yield* Scope.addFinalizer(scope, dispose)
- return { dispose }
- })
- const run = Effect.fnUntraced(function* <Event>(
- hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
- event: Event,
- ) {
- for (const hook of hooks) {
- const result = hook(event)
- if (Effect.isEffect(result)) yield* result
- }
- return event
- })
- const service = Service.of({
- hook: {
- sdk: register(
- () => sdkHooks,
- (next) => (sdkHooks = next),
- ),
- language: register(
- () => languageHooks,
- (next) => (languageHooks = next),
- ),
- },
- runSDK: (event) => run(sdkHooks, event),
- runLanguage: (event) => run(languageHooks, event),
- language: Effect.fn("AISDK.language")(function* (model) {
- const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
- const existing = languages.get(key)
- if (existing) return existing
- if (model.api.type !== "aisdk")
- return yield* new InitError({
- providerID: model.providerID,
- cause: new Error(`Unsupported api ${model.api.type}`),
- })
- const options = prepareOptions(model, model.api.package)
- const sdkKey = JSON.stringify({
- providerID: model.providerID,
- api: model.api,
- options,
- })
- const sdk =
- sdks.get(sdkKey) ??
- (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
- if (!sdk)
- return yield* new InitError({
- providerID: model.providerID,
- cause: new Error("No AISDK provider plugin returned an SDK"),
- })
- sdks.set(sdkKey, sdk)
- const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
- const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
- initError(model.providerID),
- )
- languages.set(key, language)
- return language
- }),
- })
- return service
- }),
- )
- export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
|