provider-azure.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Layer } from "effect"
  3. import { Credential } from "@opencode-ai/core/credential"
  4. import { Integration } from "@opencode-ai/core/integration"
  5. import { Database } from "@opencode-ai/core/database/database"
  6. import { Catalog } from "@opencode-ai/core/catalog"
  7. import { EventV2 } from "@opencode-ai/core/event"
  8. import { Location } from "@opencode-ai/core/location"
  9. import { PluginV2 } from "@opencode-ai/core/plugin"
  10. import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
  11. import { ProviderV2 } from "@opencode-ai/core/provider"
  12. import { AbsolutePath } from "@opencode-ai/core/schema"
  13. import { location } from "../fixture/location"
  14. import { testEffect } from "../lib/effect"
  15. import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
  16. const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
  17. const preferences = Credential.layer.pipe(Layer.provide(database))
  18. const accounts = Layer.merge(
  19. Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
  20. preferences,
  21. )
  22. const itWithAccount = testEffect(
  23. Catalog.locationLayer.pipe(
  24. Layer.provideMerge(accounts),
  25. Layer.provideMerge(EventV2.defaultLayer),
  26. Layer.provideMerge(
  27. Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
  28. ),
  29. Layer.provideMerge(npmLayer),
  30. ),
  31. )
  32. describe("AzurePlugin", () => {
  33. it.effect("resolves resourceName from env", () =>
  34. withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
  35. Effect.gen(function* () {
  36. const plugin = yield* PluginV2.Service
  37. const catalog = yield* Catalog.Service
  38. yield* plugin.add(AzurePlugin)
  39. const transform = yield* catalog.transform()
  40. yield* transform((catalog) => {
  41. catalog.provider.update(ProviderV2.ID.azure, (item) => {
  42. item.api = { type: "aisdk", package: "@ai-sdk/azure" }
  43. })
  44. })
  45. expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
  46. }),
  47. ),
  48. )
  49. it.effect("keeps explicit resourceName over env and ignores other providers", () =>
  50. withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
  51. Effect.gen(function* () {
  52. const plugin = yield* PluginV2.Service
  53. const catalog = yield* Catalog.Service
  54. yield* plugin.add(AzurePlugin)
  55. const transform = yield* catalog.transform()
  56. yield* transform((catalog) => {
  57. const azure = provider("azure", {
  58. api: { type: "aisdk", package: "@ai-sdk/azure" },
  59. request: { headers: {}, body: { resourceName: "from-config" } },
  60. })
  61. catalog.provider.update(azure.id, (item) => {
  62. item.api = azure.api
  63. item.request = azure.request
  64. })
  65. catalog.provider.update(ProviderV2.ID.openai, () => {})
  66. })
  67. expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
  68. expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
  69. }),
  70. ),
  71. )
  72. itWithAccount.effect("prefers account resourceName over env", () =>
  73. withEnv(
  74. {
  75. AZURE_RESOURCE_NAME: "from-env",
  76. },
  77. () =>
  78. Effect.gen(function* () {
  79. const plugin = yield* PluginV2.Service
  80. const credentials = yield* Credential.Service
  81. const catalog = yield* Catalog.Service
  82. yield* credentials.create({
  83. integrationID: Integration.ID.make("azure"),
  84. value: new Credential.Key({
  85. type: "key",
  86. key: "key",
  87. metadata: { resourceName: "from-account" },
  88. }),
  89. })
  90. yield* plugin.add(AzurePlugin)
  91. const transform = yield* catalog.transform()
  92. yield* transform((catalog) => {
  93. catalog.provider.update(ProviderV2.ID.azure, (item) => {
  94. item.api = { type: "aisdk", package: "@ai-sdk/azure" }
  95. })
  96. })
  97. expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account")
  98. }),
  99. ),
  100. )
  101. it.effect("falls back to env when configured resourceName is blank", () =>
  102. withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
  103. Effect.gen(function* () {
  104. const plugin = yield* PluginV2.Service
  105. const catalog = yield* Catalog.Service
  106. yield* plugin.add(AzurePlugin)
  107. const transform = yield* catalog.transform()
  108. yield* transform((catalog) => {
  109. const azure = provider("azure", {
  110. api: { type: "aisdk", package: "@ai-sdk/azure" },
  111. request: { headers: {}, body: { resourceName: "" } },
  112. })
  113. catalog.provider.update(azure.id, (item) => {
  114. item.api = azure.api
  115. item.request = azure.request
  116. })
  117. })
  118. expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
  119. }),
  120. ),
  121. )
  122. it.effect("falls back to env when configured resourceName is whitespace", () =>
  123. withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
  124. Effect.gen(function* () {
  125. const plugin = yield* PluginV2.Service
  126. const catalog = yield* Catalog.Service
  127. yield* plugin.add(AzurePlugin)
  128. const transform = yield* catalog.transform()
  129. yield* transform((catalog) => {
  130. const azure = provider("azure", {
  131. api: { type: "aisdk", package: "@ai-sdk/azure" },
  132. request: { headers: {}, body: { resourceName: " " } },
  133. })
  134. catalog.provider.update(azure.id, (item) => {
  135. item.api = azure.api
  136. item.request = azure.request
  137. })
  138. })
  139. expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
  140. }),
  141. ),
  142. )
  143. it.effect("allows configured baseURL without resourceName", () =>
  144. withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
  145. Effect.gen(function* () {
  146. const plugin = yield* PluginV2.Service
  147. yield* plugin.add(AzurePlugin)
  148. const result = yield* plugin.trigger(
  149. "aisdk.sdk",
  150. {
  151. model: model("azure", "deployment"),
  152. package: "@ai-sdk/azure",
  153. options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
  154. },
  155. {},
  156. )
  157. expect(result.sdk).toBeDefined()
  158. }),
  159. ),
  160. )
  161. it.effect("rejects missing resourceName when baseURL is not configured", () =>
  162. withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
  163. Effect.gen(function* () {
  164. const plugin = yield* PluginV2.Service
  165. yield* plugin.add(AzurePlugin)
  166. const exit = yield* plugin
  167. .trigger(
  168. "aisdk.sdk",
  169. { model: model("azure", "deployment"), package: "@ai-sdk/azure", options: { name: "azure" } },
  170. {},
  171. )
  172. .pipe(Effect.exit)
  173. expect(exit._tag).toBe("Failure")
  174. }),
  175. ),
  176. )
  177. it.effect("selects chat only for completion URLs", () =>
  178. Effect.gen(function* () {
  179. const plugin = yield* PluginV2.Service
  180. const calls: string[] = []
  181. yield* plugin.add(AzurePlugin)
  182. yield* plugin.trigger(
  183. "aisdk.language",
  184. { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
  185. {},
  186. )
  187. expect(calls).toEqual(["chat:deployment"])
  188. }),
  189. )
  190. it.effect("selects chat from per-call useCompletionUrls", () =>
  191. Effect.gen(function* () {
  192. const plugin = yield* PluginV2.Service
  193. const calls: string[] = []
  194. yield* plugin.add(AzurePlugin)
  195. yield* plugin.trigger(
  196. "aisdk.language",
  197. { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
  198. {},
  199. )
  200. expect(calls).toEqual(["chat:deployment"])
  201. }),
  202. )
  203. it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
  204. Effect.gen(function* () {
  205. const plugin = yield* PluginV2.Service
  206. const calls: string[] = []
  207. yield* plugin.add(AzurePlugin)
  208. yield* plugin.trigger(
  209. "aisdk.language",
  210. {
  211. model: model("azure", "deployment", {
  212. request: { headers: {}, body: { useCompletionUrls: true } },
  213. }),
  214. sdk: fakeSelectorSdk(calls),
  215. options: {},
  216. },
  217. {},
  218. )
  219. expect(calls).toEqual(["responses:deployment"])
  220. }),
  221. )
  222. it.effect("uses the legacy Azure selector order and provider guard", () =>
  223. Effect.gen(function* () {
  224. const plugin = yield* PluginV2.Service
  225. const calls: string[] = []
  226. yield* plugin.add(AzurePlugin)
  227. yield* plugin.trigger(
  228. "aisdk.language",
  229. { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
  230. {},
  231. )
  232. const ignored = yield* plugin.trigger(
  233. "aisdk.language",
  234. { model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
  235. {},
  236. )
  237. expect(calls).toEqual(["responses:deployment"])
  238. expect(ignored.language).toBeUndefined()
  239. }),
  240. )
  241. it.effect("falls back through the legacy Azure selector order", () =>
  242. Effect.gen(function* () {
  243. const plugin = yield* PluginV2.Service
  244. const calls: string[] = []
  245. const make = (method: string) => (id: string) => {
  246. calls.push(`${method}:${id}`)
  247. return { modelId: id, provider: method, specificationVersion: "v3" }
  248. }
  249. yield* plugin.add(AzurePlugin)
  250. yield* plugin.trigger(
  251. "aisdk.language",
  252. {
  253. model: model("azure", "messages-deployment"),
  254. sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
  255. options: {},
  256. },
  257. {},
  258. )
  259. yield* plugin.trigger(
  260. "aisdk.language",
  261. { model: model("azure", "language-deployment"), sdk: { languageModel: make("languageModel") }, options: {} },
  262. {},
  263. )
  264. expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
  265. }),
  266. )
  267. })