provider-helper.ts 4.8 KB

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