export * as AISDK from "./aisdk" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Cause, Context, Effect, Layer, Schema } from "effect" import { ModelV2 } from "./model" import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" type SDK = any 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({ async pull(ctrl) { const part = await new Promise>>((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 = { name: model.providerID, ...model.options.aisdk.provider } if (model.endpoint.type === "aisdk" && model.endpoint.url) options.baseURL = model.endpoint.url const customFetch = options.fetch const chunkTimeout = options.chunkTimeout delete options.chunkTimeout options.fetch = async (input: Parameters[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") && 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()("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 language: (model: ModelV2.Info) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/AISDK") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const plugin = yield* PluginV2.Service const languages = new Map() const sdks = new Map() return Service.of({ language: Effect.fn("AISDK.language")(function* (model) { const key = `${model.providerID}/${model.id}/${model.options.variant ?? "default"}` const existing = languages.get(key) if (existing) return existing if (model.endpoint.type !== "aisdk") return yield* new InitError({ providerID: model.providerID, cause: new Error(`Unsupported endpoint ${model.endpoint.type}`), }) const options = prepareOptions(model, model.endpoint.package) const sdkKey = JSON.stringify({ providerID: model.providerID, endpoint: model.endpoint, options, }) const sdk = sdks.get(sdkKey) ?? (yield* plugin .trigger("aisdk.sdk", { model, package: model.endpoint.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* plugin .trigger( "aisdk.language", { model, sdk, options, }, {}, ) .pipe(initError(model.providerID)) const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.apiID)).pipe( initError(model.providerID), ) languages.set(key, language) return language }), }) }), ) export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))