| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- 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 { EventV2 } from "./event"
- 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<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") && 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 language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
- }
- export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const languages = new Map<string, LanguageModelV3>()
- const sdks = new Map<string, SDK>()
- return Service.of({
- 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* plugin
- .trigger("aisdk.sdk", { 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* plugin
- .trigger(
- "aisdk.language",
- {
- 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
- }),
- })
- }),
- )
- export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
|