aisdk.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. export * as AISDK from "./aisdk"
  2. import type { LanguageModelV3 } from "@ai-sdk/provider"
  3. import { Cause, Context, Effect, Layer, Schema } from "effect"
  4. import { ModelV2 } from "./model"
  5. import { EventV2 } from "./event"
  6. import { PluginV2 } from "./plugin"
  7. import { ProviderV2 } from "./provider"
  8. type SDK = any
  9. function wrapSSE(res: Response, ms: number, ctl: AbortController) {
  10. if (typeof ms !== "number" || ms <= 0) return res
  11. if (!res.body) return res
  12. if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
  13. const reader = res.body.getReader()
  14. const body = new ReadableStream<Uint8Array>({
  15. async pull(ctrl) {
  16. const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
  17. const id = setTimeout(() => {
  18. const err = new Error("SSE read timed out")
  19. ctl.abort(err)
  20. void reader.cancel(err)
  21. reject(err)
  22. }, ms)
  23. reader.read().then(
  24. (part) => {
  25. clearTimeout(id)
  26. resolve(part)
  27. },
  28. (err) => {
  29. clearTimeout(id)
  30. reject(err)
  31. },
  32. )
  33. })
  34. if (part.done) {
  35. ctrl.close()
  36. return
  37. }
  38. ctrl.enqueue(part.value)
  39. },
  40. async cancel(reason) {
  41. ctl.abort(reason)
  42. await reader.cancel(reason)
  43. },
  44. })
  45. return new Response(body, {
  46. headers: new Headers(res.headers),
  47. status: res.status,
  48. statusText: res.statusText,
  49. })
  50. }
  51. function prepareOptions(model: ModelV2.Info, pkg: string) {
  52. const options: Record<string, any> = {
  53. name: model.providerID,
  54. ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
  55. ...model.request.body,
  56. }
  57. if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
  58. const customFetch = options.fetch
  59. const chunkTimeout = options.chunkTimeout
  60. delete options.chunkTimeout
  61. options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  62. const opts = { ...(init ?? {}) }
  63. const signals = [
  64. opts.signal,
  65. typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined,
  66. options.timeout !== undefined && options.timeout !== null && options.timeout !== false
  67. ? AbortSignal.timeout(options.timeout)
  68. : undefined,
  69. ].filter((item): item is AbortSignal | AbortController => Boolean(item))
  70. const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController)
  71. const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item))
  72. if (abortSignals.length === 1) opts.signal = abortSignals[0]
  73. if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
  74. if ((pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure") && opts.body && opts.method === "POST") {
  75. const body = JSON.parse(opts.body as string)
  76. if (body.store !== true && Array.isArray(body.input)) {
  77. for (const item of body.input) {
  78. if ("id" in item) delete item.id
  79. }
  80. opts.body = JSON.stringify(body)
  81. }
  82. }
  83. const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
  84. ...opts,
  85. timeout: false,
  86. })
  87. if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
  88. return wrapSSE(res, chunkTimeout, chunkAbortCtl)
  89. }
  90. return options
  91. }
  92. export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
  93. providerID: ProviderV2.ID,
  94. cause: Schema.Defect,
  95. }) {}
  96. function initError(providerID: ProviderV2.ID) {
  97. return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
  98. }
  99. export interface Interface {
  100. readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
  101. }
  102. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
  103. export const layer = Layer.effect(
  104. Service,
  105. Effect.gen(function* () {
  106. const plugin = yield* PluginV2.Service
  107. const languages = new Map<string, LanguageModelV3>()
  108. const sdks = new Map<string, SDK>()
  109. return Service.of({
  110. language: Effect.fn("AISDK.language")(function* (model) {
  111. const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
  112. const existing = languages.get(key)
  113. if (existing) return existing
  114. if (model.api.type !== "aisdk")
  115. return yield* new InitError({
  116. providerID: model.providerID,
  117. cause: new Error(`Unsupported api ${model.api.type}`),
  118. })
  119. const options = prepareOptions(model, model.api.package)
  120. const sdkKey = JSON.stringify({
  121. providerID: model.providerID,
  122. api: model.api,
  123. options,
  124. })
  125. const sdk =
  126. sdks.get(sdkKey) ??
  127. (yield* plugin
  128. .trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
  129. .pipe(initError(model.providerID))).sdk
  130. if (!sdk)
  131. return yield* new InitError({
  132. providerID: model.providerID,
  133. cause: new Error("No AISDK provider plugin returned an SDK"),
  134. })
  135. sdks.set(sdkKey, sdk)
  136. const result = yield* plugin
  137. .trigger(
  138. "aisdk.language",
  139. {
  140. model,
  141. sdk,
  142. options,
  143. },
  144. {},
  145. )
  146. .pipe(initError(model.providerID))
  147. const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
  148. initError(model.providerID),
  149. )
  150. languages.set(key, language)
  151. return language
  152. }),
  153. })
  154. }),
  155. )
  156. export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))