provider.test.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Option, Schema } from "effect"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { Config } from "@opencode-ai/core/config"
  5. import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
  6. import { ModelV2 } from "@opencode-ai/core/model"
  7. import { PluginV2 } from "@opencode-ai/core/plugin"
  8. import { ProviderV2 } from "@opencode-ai/core/provider"
  9. import { it } from "../plugin/provider-helper"
  10. function request(headers: Record<string, string>, variant?: string) {
  11. return {
  12. headers,
  13. variant,
  14. }
  15. }
  16. const decode = Schema.decodeUnknownSync(Config.Info)
  17. describe("ConfigProviderPlugin.Plugin", () => {
  18. it.effect("partitions existing model variant bodies without changing config shape", () =>
  19. Effect.gen(function* () {
  20. const catalog = yield* Catalog.Service
  21. const plugin = yield* PluginV2.Service
  22. const providerID = ProviderV2.ID.opencode
  23. const modelID = ModelV2.ID.make("alpha-gpt-next")
  24. const config = Config.Service.of({
  25. entries: () =>
  26. Effect.succeed([
  27. new Config.Document({
  28. type: "document",
  29. info: decode({
  30. providers: {
  31. opencode: {
  32. api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
  33. models: {
  34. "alpha-gpt-next": {
  35. variants: [
  36. {
  37. id: "high",
  38. body: {
  39. reasoningEffort: "high",
  40. reasoningSummary: "auto",
  41. include: ["reasoning.encrypted_content"],
  42. },
  43. },
  44. ],
  45. },
  46. },
  47. },
  48. },
  49. }),
  50. }),
  51. ]),
  52. })
  53. yield* plugin.add({
  54. ...ConfigProviderPlugin.Plugin,
  55. effect: ConfigProviderPlugin.Plugin.effect.pipe(
  56. Effect.provideService(Config.Service, config),
  57. Effect.provideService(Catalog.Service, catalog),
  58. ),
  59. })
  60. const model = yield* catalog.model.get(providerID, modelID)
  61. expect(model.variants).toMatchObject([
  62. {
  63. id: "high",
  64. body: {},
  65. options: {
  66. reasoningEffort: "high",
  67. reasoningSummary: "auto",
  68. include: ["reasoning.encrypted_content"],
  69. },
  70. },
  71. ])
  72. }),
  73. )
  74. it.effect("uses the effective provider package across layered config", () =>
  75. Effect.gen(function* () {
  76. const catalog = yield* Catalog.Service
  77. const plugin = yield* PluginV2.Service
  78. const providerID = ProviderV2.ID.opencode
  79. const modelID = ModelV2.ID.make("alpha-gpt-next")
  80. const config = Config.Service.of({
  81. entries: () =>
  82. Effect.succeed([
  83. new Config.Document({
  84. type: "document",
  85. info: decode({
  86. providers: {
  87. opencode: {
  88. api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
  89. },
  90. },
  91. }),
  92. }),
  93. new Config.Document({
  94. type: "document",
  95. info: decode({
  96. providers: {
  97. opencode: {
  98. models: {
  99. "alpha-gpt-next": {
  100. variants: [{ id: "high", body: { reasoningEffort: "high" } }],
  101. },
  102. },
  103. },
  104. },
  105. }),
  106. }),
  107. ]),
  108. })
  109. yield* plugin.add({
  110. ...ConfigProviderPlugin.Plugin,
  111. effect: ConfigProviderPlugin.Plugin.effect.pipe(
  112. Effect.provideService(Config.Service, config),
  113. Effect.provideService(Catalog.Service, catalog),
  114. ),
  115. })
  116. const model = yield* catalog.model.get(providerID, modelID)
  117. expect(model.variants[0]).toMatchObject({
  118. id: "high",
  119. body: {},
  120. options: { reasoningEffort: "high" },
  121. })
  122. }),
  123. )
  124. it.effect("loads configured providers and applies later model overrides", () =>
  125. Effect.gen(function* () {
  126. const catalog = yield* Catalog.Service
  127. const plugin = yield* PluginV2.Service
  128. const providerID = ProviderV2.ID.make("custom")
  129. const modelID = ModelV2.ID.make("chat")
  130. const config = Config.Service.of({
  131. entries: () =>
  132. Effect.succeed([
  133. new Config.Document({
  134. type: "document",
  135. info: decode({
  136. model: "custom/first",
  137. providers: {
  138. custom: {
  139. name: "Configured",
  140. env: ["CUSTOM_API_KEY"],
  141. api: { type: "native", settings: {} },
  142. request: request({ first: "first", shared: "first" }),
  143. models: {
  144. chat: {
  145. name: "First",
  146. capabilities: { tools: true, input: ["text"], output: ["text"] },
  147. disabled: true,
  148. limit: { context: 100, output: 50 },
  149. cost: { input: 1, output: 2 },
  150. request: request({ first: "first", shared: "first" }, "retained"),
  151. variants: [
  152. {
  153. id: "fast",
  154. headers: { first: "first", shared: "first" },
  155. },
  156. ],
  157. },
  158. },
  159. },
  160. },
  161. }),
  162. }),
  163. new Config.Document({
  164. type: "document",
  165. info: decode({
  166. model: "custom/default",
  167. providers: {
  168. custom: {
  169. api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
  170. request: request({ last: "last", shared: "last" }),
  171. models: {
  172. default: {
  173. name: "Default",
  174. },
  175. chat: {
  176. api: { id: "api-chat" },
  177. name: "Last",
  178. limit: { output: 75 },
  179. request: request({ last: "last", shared: "last" }),
  180. variants: [
  181. {
  182. id: "fast",
  183. headers: { last: "last", shared: "last" },
  184. },
  185. {
  186. id: "slow",
  187. headers: { slow: "slow" },
  188. },
  189. ],
  190. },
  191. },
  192. },
  193. },
  194. }),
  195. }),
  196. new Config.Document({
  197. type: "document",
  198. info: decode({
  199. providers: {
  200. custom: { name: "Renamed" },
  201. },
  202. }),
  203. }),
  204. ]),
  205. })
  206. yield* plugin.add({
  207. ...ConfigProviderPlugin.Plugin,
  208. effect: ConfigProviderPlugin.Plugin.effect.pipe(
  209. Effect.provideService(Config.Service, config),
  210. Effect.provideService(Catalog.Service, catalog),
  211. ),
  212. })
  213. const provider = yield* catalog.provider.get(providerID)
  214. const model = yield* catalog.model.get(providerID, modelID)
  215. expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
  216. expect(provider.name).toBe("Renamed")
  217. expect(provider.env).toEqual(["CUSTOM_API_KEY"])
  218. expect(provider.enabled).toEqual({ via: "custom", data: {} })
  219. expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
  220. expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
  221. expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
  222. expect(model.name).toBe("Last")
  223. expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
  224. expect(model.enabled).toBe(false)
  225. expect(model.limit).toEqual({ context: 100, output: 75 })
  226. expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
  227. expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
  228. expect(model.request.variant).toBe("retained")
  229. expect(model.variants.map((variant) => variant.id)).toEqual([
  230. ModelV2.VariantID.make("fast"),
  231. ModelV2.VariantID.make("slow"),
  232. ])
  233. expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
  234. expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
  235. }),
  236. )
  237. })