provider-helper.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { Npm } from "@opencode-ai/core/npm"
  2. import type { Plugin } from "@opencode-ai/plugin/v2/effect"
  3. import type { LanguageModelV3 } from "@ai-sdk/provider"
  4. import { expect } from "bun:test"
  5. import { Effect, Layer, Option } from "effect"
  6. import { Catalog } from "@opencode-ai/core/catalog"
  7. import { Integration } from "@opencode-ai/core/integration"
  8. import { Credential } from "@opencode-ai/core/credential"
  9. import { EventV2 } from "@opencode-ai/core/event"
  10. import { Location } from "@opencode-ai/core/location"
  11. import { ModelV2 } from "@opencode-ai/core/model"
  12. import { PluginV2 } from "@opencode-ai/core/plugin"
  13. import { ProviderV2 } from "@opencode-ai/core/provider"
  14. import { AbsolutePath } from "@opencode-ai/core/schema"
  15. import { location } from "../fixture/location"
  16. import { testEffect } from "../lib/effect"
  17. import { aisdkHost, catalogHost, host, integrationHost } from "./host"
  18. export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
  19. export function required<T>(value: T | undefined): T {
  20. if (value === undefined) throw new Error("Expected value")
  21. return value
  22. }
  23. const locationLayer = Layer.succeed(
  24. Location.Service,
  25. Location.Service.of(location({ directory: AbsolutePath.make("test") })),
  26. )
  27. export const npmLayer = Layer.succeed(
  28. Npm.Service,
  29. Npm.Service.of({
  30. add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
  31. install: () => Effect.void,
  32. which: () => Effect.succeed(undefined),
  33. }),
  34. )
  35. export const catalogLayer = Layer.succeed(
  36. Catalog.Service,
  37. Catalog.Service.of({
  38. transform: (_transform) => Effect.die("unexpected catalog.transform"),
  39. rebuild: () => Effect.die("unexpected catalog.rebuild"),
  40. provider: {
  41. get: () => Effect.die("unexpected provider.get"),
  42. all: () => Effect.succeed([]),
  43. available: () => Effect.succeed([]),
  44. },
  45. model: {
  46. get: () => Effect.die("unexpected model.get"),
  47. all: () => Effect.succeed([]),
  48. available: () => Effect.succeed([]),
  49. default: () => Effect.succeed(undefined),
  50. small: () => Effect.succeed(undefined),
  51. },
  52. }),
  53. )
  54. const integrations = Integration.locationLayer.pipe(
  55. Layer.provide(EventV2.defaultLayer),
  56. Layer.provide(
  57. Layer.mock(Credential.Service)({
  58. create: () => Effect.die("unexpected credential creation"),
  59. all: () => Effect.succeed([]),
  60. list: () => Effect.succeed([]),
  61. }),
  62. ),
  63. )
  64. export const it = testEffect(
  65. Catalog.locationLayer.pipe(
  66. Layer.provideMerge(integrations),
  67. Layer.provideMerge(
  68. Layer.mock(Credential.Service)({
  69. all: () => Effect.succeed([]),
  70. }),
  71. ),
  72. Layer.provideMerge(EventV2.defaultLayer),
  73. Layer.provideMerge(locationLayer),
  74. Layer.provideMerge(npmLayer),
  75. Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
  76. ),
  77. )
  78. export function addPlugin(plugin: PluginV2.Interface, definition: Plugin<any>) {
  79. return Effect.gen(function* () {
  80. const catalog = yield* Effect.serviceOption(Catalog.Service)
  81. const integration = yield* Effect.serviceOption(Integration.Service)
  82. const npm = yield* Effect.serviceOption(Npm.Service)
  83. const effect =
  84. typeof definition.effect === "function"
  85. ? definition.effect(
  86. host({
  87. aisdk: aisdkHost(plugin),
  88. ...(Option.isSome(catalog) ? { catalog: catalogHost(catalog.value) } : {}),
  89. ...(Option.isSome(integration) ? { integration: integrationHost(integration.value) } : {}),
  90. ...(Option.isSome(npm) ? { npm: npm.value } : {}),
  91. }),
  92. )
  93. : definition.effect
  94. yield* plugin.add({ id: definition.id, effect })
  95. })
  96. }
  97. type ProviderInput = Partial<Omit<ProviderV2.Info, "api" | "request">> & {
  98. api?: ProviderV2.Api
  99. request?: ProviderV2.Request
  100. }
  101. type ModelInput = Partial<Omit<ModelV2.Info, "api" | "request">> & {
  102. api?: (ProviderV2.Api & { id?: ModelV2.ID }) | { id: ModelV2.ID }
  103. request?: ModelV2.Info["request"]
  104. }
  105. export function provider(providerID: string, options?: ProviderInput) {
  106. return new ProviderV2.Info({
  107. ...ProviderV2.Info.empty(ProviderV2.ID.make(providerID)),
  108. api: options?.api ?? {
  109. type: "aisdk",
  110. package: "test-provider",
  111. },
  112. ...options,
  113. request: {
  114. headers: {},
  115. body: {},
  116. ...options?.request,
  117. },
  118. })
  119. }
  120. export function model(providerID: string, modelID: string, options?: ModelInput) {
  121. return new ModelV2.Info({
  122. ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
  123. ...options,
  124. api:
  125. options?.api && "type" in options.api
  126. ? { id: ModelV2.ID.make(modelID), ...options.api }
  127. : {
  128. id: ModelV2.ID.make(modelID),
  129. ...options?.api,
  130. type: "aisdk",
  131. package: "test-provider",
  132. },
  133. request: {
  134. headers: {},
  135. body: {},
  136. ...options?.request,
  137. },
  138. })
  139. }
  140. export function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
  141. return Effect.acquireUseRelease(
  142. Effect.sync(() => {
  143. const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
  144. for (const [key, value] of Object.entries(vars)) {
  145. if (value === undefined) delete process.env[key]
  146. else process.env[key] = value
  147. }
  148. return previous
  149. }),
  150. () => fx(),
  151. (previous) =>
  152. Effect.sync(() => {
  153. for (const [key, value] of Object.entries(previous)) {
  154. if (value === undefined) delete process.env[key]
  155. else process.env[key] = value
  156. }
  157. }),
  158. )
  159. }
  160. export function fakeSelectorSdk(calls: string[]) {
  161. const make = (method: string) => (id: string) => {
  162. calls.push(`${method}:${id}`)
  163. return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
  164. }
  165. return {
  166. responses: make("responses"),
  167. messages: make("messages"),
  168. chat: make("chat"),
  169. languageModel: make("languageModel"),
  170. }
  171. }
  172. export function expectPluginRegistered(ids: string[], id: string) {
  173. expect(ids).toContain(PluginV2.ID.make(id))
  174. }