aisdk.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 (
  75. (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") &&
  76. opts.body &&
  77. opts.method === "POST"
  78. ) {
  79. const body = JSON.parse(opts.body as string)
  80. if (body.store !== true && Array.isArray(body.input)) {
  81. for (const item of body.input) {
  82. if ("id" in item) delete item.id
  83. }
  84. opts.body = JSON.stringify(body)
  85. }
  86. }
  87. const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
  88. ...opts,
  89. timeout: false,
  90. })
  91. if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
  92. return wrapSSE(res, chunkTimeout, chunkAbortCtl)
  93. }
  94. return options
  95. }
  96. export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
  97. providerID: ProviderV2.ID,
  98. cause: Schema.Defect,
  99. }) {}
  100. function initError(providerID: ProviderV2.ID) {
  101. return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
  102. }
  103. export interface Interface {
  104. readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
  105. }
  106. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
  107. export const layer = Layer.effect(
  108. Service,
  109. Effect.gen(function* () {
  110. const plugin = yield* PluginV2.Service
  111. const languages = new Map<string, LanguageModelV3>()
  112. const sdks = new Map<string, SDK>()
  113. return Service.of({
  114. language: Effect.fn("AISDK.language")(function* (model) {
  115. const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
  116. const existing = languages.get(key)
  117. if (existing) return existing
  118. if (model.api.type !== "aisdk")
  119. return yield* new InitError({
  120. providerID: model.providerID,
  121. cause: new Error(`Unsupported api ${model.api.type}`),
  122. })
  123. const options = prepareOptions(model, model.api.package)
  124. const sdkKey = JSON.stringify({
  125. providerID: model.providerID,
  126. api: model.api,
  127. options,
  128. })
  129. const sdk =
  130. sdks.get(sdkKey) ??
  131. (yield* plugin
  132. .trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
  133. .pipe(initError(model.providerID))).sdk
  134. if (!sdk)
  135. return yield* new InitError({
  136. providerID: model.providerID,
  137. cause: new Error("No AISDK provider plugin returned an SDK"),
  138. })
  139. sdks.set(sdkKey, sdk)
  140. const result = yield* plugin
  141. .trigger(
  142. "aisdk.language",
  143. {
  144. model,
  145. sdk,
  146. options,
  147. },
  148. {},
  149. )
  150. .pipe(initError(model.providerID))
  151. const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
  152. initError(model.providerID),
  153. )
  154. languages.set(key, language)
  155. return language
  156. }),
  157. })
  158. }),
  159. )
  160. export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))