provider-opencode.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { describe, expect } from "bun:test"
  2. import { DateTime, Effect, Option } from "effect"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { ModelV2 } from "@opencode-ai/core/model"
  5. import { PluginV2 } from "@opencode-ai/core/plugin"
  6. import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
  7. import { ProviderV2 } from "@opencode-ai/core/provider"
  8. import { it, model, provider, withEnv } from "./provider-helper"
  9. const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
  10. describe("OpencodePlugin", () => {
  11. it.effect("uses a public key and cancels paid models without credentials", () =>
  12. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  13. Effect.gen(function* () {
  14. const plugin = yield* PluginV2.Service
  15. yield* plugin.add(OpencodePlugin)
  16. const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
  17. const paid = yield* plugin.trigger(
  18. "model.update",
  19. {},
  20. { model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
  21. )
  22. expect(updated.provider.options.aisdk.provider.apiKey).toBe("public")
  23. expect(paid.cancel).toBe(true)
  24. }),
  25. ),
  26. )
  27. it.effect("keeps free models without credentials", () =>
  28. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  29. Effect.gen(function* () {
  30. const plugin = yield* PluginV2.Service
  31. yield* plugin.add(OpencodePlugin)
  32. yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
  33. const free = yield* plugin.trigger(
  34. "model.update",
  35. {},
  36. { model: model("opencode", "free", { cost: cost(0) }), cancel: false },
  37. )
  38. expect(free.cancel).toBe(false)
  39. }),
  40. ),
  41. )
  42. it.effect("treats output-only cost as free without credentials", () =>
  43. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  44. Effect.gen(function* () {
  45. const plugin = yield* PluginV2.Service
  46. yield* plugin.add(OpencodePlugin)
  47. yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
  48. const outputOnly = yield* plugin.trigger(
  49. "model.update",
  50. {},
  51. { model: model("opencode", "output-only", { cost: cost(0, 1) }), cancel: false },
  52. )
  53. expect(outputOnly.cancel).toBe(false)
  54. }),
  55. ),
  56. )
  57. it.effect("uses OPENCODE_API_KEY as credentials", () =>
  58. withEnv({ OPENCODE_API_KEY: "secret" }, () =>
  59. Effect.gen(function* () {
  60. const plugin = yield* PluginV2.Service
  61. yield* plugin.add(OpencodePlugin)
  62. const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
  63. const paid = yield* plugin.trigger(
  64. "model.update",
  65. {},
  66. { model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
  67. )
  68. expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
  69. expect(paid.cancel).toBe(false)
  70. }),
  71. ),
  72. )
  73. it.effect("uses configured provider env vars as credentials", () =>
  74. withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () =>
  75. Effect.gen(function* () {
  76. const plugin = yield* PluginV2.Service
  77. yield* plugin.add(OpencodePlugin)
  78. const updated = yield* plugin.trigger(
  79. "provider.update",
  80. {},
  81. { provider: provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] }), cancel: false },
  82. )
  83. const paid = yield* plugin.trigger(
  84. "model.update",
  85. {},
  86. { model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
  87. )
  88. expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
  89. expect(paid.cancel).toBe(false)
  90. }),
  91. ),
  92. )
  93. it.effect("uses configured apiKey as credentials", () =>
  94. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  95. Effect.gen(function* () {
  96. const plugin = yield* PluginV2.Service
  97. yield* plugin.add(OpencodePlugin)
  98. const updated = yield* plugin.trigger(
  99. "provider.update",
  100. {},
  101. {
  102. provider: provider("opencode", {
  103. options: {
  104. headers: {},
  105. body: {},
  106. aisdk: {
  107. provider: { apiKey: "configured" },
  108. request: {},
  109. },
  110. },
  111. }),
  112. cancel: false,
  113. },
  114. )
  115. const paid = yield* plugin.trigger(
  116. "model.update",
  117. {},
  118. { model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
  119. )
  120. expect(updated.provider.options.aisdk.provider.apiKey).toBe("configured")
  121. expect(paid.cancel).toBe(false)
  122. }),
  123. ),
  124. )
  125. it.effect("uses auth-enabled providers as credentials", () =>
  126. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  127. Effect.gen(function* () {
  128. const plugin = yield* PluginV2.Service
  129. yield* plugin.add(OpencodePlugin)
  130. const updated = yield* plugin.trigger(
  131. "provider.update",
  132. {},
  133. { provider: provider("opencode", { enabled: { via: "auth", service: "opencode" } }), cancel: false },
  134. )
  135. const paid = yield* plugin.trigger(
  136. "model.update",
  137. {},
  138. { model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
  139. )
  140. expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
  141. expect(paid.cancel).toBe(false)
  142. }),
  143. ),
  144. )
  145. it.effect("ignores non-opencode providers and models", () =>
  146. withEnv({ OPENCODE_API_KEY: undefined }, () =>
  147. Effect.gen(function* () {
  148. const plugin = yield* PluginV2.Service
  149. yield* plugin.add(OpencodePlugin)
  150. const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
  151. const paid = yield* plugin.trigger(
  152. "model.update",
  153. {},
  154. { model: model("openai", "paid", { cost: cost(1) }), cancel: false },
  155. )
  156. expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
  157. expect(paid.cancel).toBe(false)
  158. }),
  159. ),
  160. )
  161. it.effect("prefers gpt-5-nano as the opencode small model", () =>
  162. Effect.gen(function* () {
  163. const catalog = yield* Catalog.Service
  164. const providerID = ProviderV2.ID.opencode
  165. yield* catalog.provider.update(providerID, () => {})
  166. yield* catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => {
  167. model.capabilities.input = ["text"]
  168. model.capabilities.output = ["text"]
  169. model.cost = cost(1, 1)
  170. model.time.released = DateTime.makeUnsafe(Date.now())
  171. })
  172. yield* catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => {
  173. model.capabilities.input = ["text"]
  174. model.capabilities.output = ["text"]
  175. model.cost = cost(10, 10)
  176. model.time.released = DateTime.makeUnsafe(Date.now())
  177. })
  178. const selected = yield* catalog.model.small(providerID)
  179. expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
  180. }).pipe(Effect.provide(Catalog.defaultLayer)),
  181. )
  182. })