provider-google-vertex.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { describe, expect, mock } from "bun:test"
  2. import { Effect } from "effect"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { PluginV2 } from "@opencode-ai/core/plugin"
  5. import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
  6. import { ProviderV2 } from "@opencode-ai/core/provider"
  7. import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
  8. const vertexOptions: Record<string, any>[] = []
  9. void mock.module("@ai-sdk/google-vertex", () => ({
  10. createVertex: (options: Record<string, any>) => {
  11. vertexOptions.push(options)
  12. return {
  13. languageModel: (modelID: string) => ({ modelID, provider: "google-vertex", specificationVersion: "v3" }),
  14. }
  15. },
  16. }))
  17. void mock.module("google-auth-library", () => ({
  18. GoogleAuth: class {
  19. async getApplicationDefault() {
  20. return {
  21. credential: {
  22. async getAccessToken() {
  23. return { token: "vertex-token" }
  24. },
  25. },
  26. }
  27. }
  28. },
  29. }))
  30. describe("GoogleVertexPlugin", () => {
  31. it.effect("resolves project and location from env using legacy precedence", () =>
  32. withEnv(
  33. {
  34. GOOGLE_CLOUD_PROJECT: "google-cloud-project",
  35. GCP_PROJECT: "gcp-project",
  36. GCLOUD_PROJECT: "gcloud-project",
  37. GOOGLE_VERTEX_LOCATION: "google-vertex-location",
  38. GOOGLE_CLOUD_LOCATION: "google-cloud-location",
  39. VERTEX_LOCATION: "vertex-location",
  40. },
  41. () =>
  42. Effect.gen(function* () {
  43. const plugin = yield* PluginV2.Service
  44. const catalog = yield* Catalog.Service
  45. yield* plugin.add(GoogleVertexPlugin)
  46. const load = yield* catalog.loader()
  47. yield* load((catalog) =>
  48. catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
  49. provider.endpoint = {
  50. type: "aisdk",
  51. package: "@ai-sdk/openai-compatible",
  52. url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
  53. }
  54. }),
  55. )
  56. const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
  57. expect(provider.options.aisdk.provider.project).toBe("google-cloud-project")
  58. expect(provider.options.aisdk.provider.location).toBe("google-vertex-location")
  59. expect(provider.endpoint).toEqual({
  60. type: "aisdk",
  61. package: "@ai-sdk/openai-compatible",
  62. url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
  63. })
  64. }),
  65. ),
  66. )
  67. it.effect("resolves the advertised GOOGLE_VERTEX_PROJECT env for provider updates and SDKs", () =>
  68. withEnv(
  69. {
  70. GOOGLE_VERTEX_PROJECT: "vertex-project",
  71. GOOGLE_CLOUD_PROJECT: undefined,
  72. GCP_PROJECT: undefined,
  73. GCLOUD_PROJECT: undefined,
  74. GOOGLE_VERTEX_LOCATION: "europe-west4",
  75. GOOGLE_CLOUD_LOCATION: undefined,
  76. VERTEX_LOCATION: undefined,
  77. },
  78. () =>
  79. Effect.gen(function* () {
  80. vertexOptions.length = 0
  81. const plugin = yield* PluginV2.Service
  82. const catalog = yield* Catalog.Service
  83. yield* plugin.add(GoogleVertexPlugin)
  84. const load = yield* catalog.loader()
  85. yield* load((catalog) =>
  86. catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
  87. provider.endpoint = {
  88. type: "aisdk",
  89. package: "@ai-sdk/openai-compatible",
  90. url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
  91. }
  92. }),
  93. )
  94. const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
  95. yield* plugin.trigger(
  96. "aisdk.sdk",
  97. {
  98. model: model("google-vertex", "gemini", {
  99. endpoint: { type: "aisdk", package: "@ai-sdk/google-vertex" },
  100. }),
  101. package: "@ai-sdk/google-vertex",
  102. options: { name: "google-vertex" },
  103. },
  104. {},
  105. )
  106. expect(provider.options.aisdk.provider.project).toBe("vertex-project")
  107. expect(provider.endpoint).toEqual({
  108. type: "aisdk",
  109. package: "@ai-sdk/openai-compatible",
  110. url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
  111. })
  112. expect(vertexOptions[0].project).toBe("vertex-project")
  113. expect(vertexOptions[0].location).toBe("europe-west4")
  114. }),
  115. ),
  116. )
  117. it.effect("keeps configured project and location over env and uses global endpoint", () =>
  118. withEnv(
  119. {
  120. GOOGLE_CLOUD_PROJECT: "env-project",
  121. GCP_PROJECT: "env-gcp-project",
  122. GCLOUD_PROJECT: "env-gcloud-project",
  123. GOOGLE_VERTEX_LOCATION: "env-location",
  124. GOOGLE_CLOUD_LOCATION: "env-google-cloud-location",
  125. VERTEX_LOCATION: "env-vertex-location",
  126. },
  127. () =>
  128. Effect.gen(function* () {
  129. const plugin = yield* PluginV2.Service
  130. const catalog = yield* Catalog.Service
  131. yield* plugin.add(GoogleVertexPlugin)
  132. const load = yield* catalog.loader()
  133. yield* load((catalog) =>
  134. catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
  135. provider.endpoint = {
  136. type: "aisdk",
  137. package: "@ai-sdk/openai-compatible",
  138. url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
  139. }
  140. provider.options.aisdk.provider.project = "config-project"
  141. provider.options.aisdk.provider.location = "global"
  142. }),
  143. )
  144. const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
  145. expect(provider.options.aisdk.provider.project).toBe("config-project")
  146. expect(provider.options.aisdk.provider.location).toBe("global")
  147. expect(provider.endpoint).toEqual({
  148. type: "aisdk",
  149. package: "@ai-sdk/openai-compatible",
  150. url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
  151. })
  152. }),
  153. ),
  154. )
  155. it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () =>
  156. Effect.gen(function* () {
  157. const plugin = yield* PluginV2.Service
  158. const catalog = yield* Catalog.Service
  159. yield* plugin.add(GoogleVertexPlugin)
  160. const load = yield* catalog.loader()
  161. yield* load((catalog) =>
  162. catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
  163. provider.endpoint = {
  164. type: "aisdk",
  165. package: "@ai-sdk/openai-compatible",
  166. url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
  167. }
  168. provider.options.aisdk.provider.project = "config-project"
  169. provider.options.aisdk.provider.location = "eu"
  170. }),
  171. )
  172. const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
  173. expect(provider.endpoint).toEqual({
  174. type: "aisdk",
  175. package: "@ai-sdk/openai-compatible",
  176. url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
  177. })
  178. }),
  179. )
  180. it.effect("defaults location to us-central1 when only project is configured", () =>
  181. withEnv(
  182. {
  183. GOOGLE_CLOUD_PROJECT: undefined,
  184. GCP_PROJECT: undefined,
  185. GCLOUD_PROJECT: undefined,
  186. GOOGLE_VERTEX_LOCATION: undefined,
  187. GOOGLE_CLOUD_LOCATION: undefined,
  188. VERTEX_LOCATION: undefined,
  189. },
  190. () =>
  191. Effect.gen(function* () {
  192. const plugin = yield* PluginV2.Service
  193. const catalog = yield* Catalog.Service
  194. yield* plugin.add(GoogleVertexPlugin)
  195. const load = yield* catalog.loader()
  196. yield* load((catalog) =>
  197. catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
  198. provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex" }
  199. provider.options.aisdk.provider.project = "config-project"
  200. }),
  201. )
  202. const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
  203. expect(provider.options.aisdk.provider.project).toBe("config-project")
  204. expect(provider.options.aisdk.provider.location).toBe("us-central1")
  205. }),
  206. ),
  207. )
  208. it.effect("does not pass Google auth fetch to the native Vertex SDK", () =>
  209. withEnv(
  210. {
  211. GOOGLE_CLOUD_PROJECT: "env-project",
  212. GOOGLE_VERTEX_LOCATION: "env-location",
  213. },
  214. () =>
  215. Effect.gen(function* () {
  216. vertexOptions.length = 0
  217. const plugin = yield* PluginV2.Service
  218. yield* plugin.add(GoogleVertexPlugin)
  219. yield* plugin.trigger(
  220. "aisdk.sdk",
  221. {
  222. model: model("google-vertex", "gemini", {
  223. endpoint: { type: "aisdk", package: "@ai-sdk/google-vertex" },
  224. }),
  225. package: "@ai-sdk/google-vertex",
  226. options: { name: "google-vertex" },
  227. },
  228. {},
  229. )
  230. expect(vertexOptions).toHaveLength(1)
  231. expect(vertexOptions[0].project).toBe("env-project")
  232. expect(vertexOptions[0].location).toBe("env-location")
  233. expect(vertexOptions[0].fetch).toBeUndefined()
  234. }),
  235. ),
  236. )
  237. it.effect("keeps Google auth fetch for OpenAI-compatible Vertex endpoints", () =>
  238. Effect.gen(function* () {
  239. const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
  240. const plugin = yield* PluginV2.Service
  241. yield* plugin.add(GoogleVertexPlugin)
  242. yield* plugin.add({
  243. id: PluginV2.ID.make("capture-openai-compatible"),
  244. effect: Effect.succeed({
  245. "aisdk.sdk": (evt) =>
  246. Effect.promise(async () => {
  247. if (evt.model.providerID !== "google-vertex") return
  248. if (evt.package !== "@ai-sdk/openai-compatible") return
  249. expect(typeof evt.options.fetch).toBe("function")
  250. await evt.options.fetch("https://vertex.example", {
  251. headers: { "x-test": "1" },
  252. })
  253. }),
  254. }),
  255. })
  256. const originalFetch = fetch
  257. ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = (async (
  258. input: Parameters<typeof fetch>[0],
  259. init?: RequestInit,
  260. ) => {
  261. fetchCalls.push({ input, init })
  262. return new Response("ok")
  263. }) as typeof fetch
  264. yield* Effect.acquireUseRelease(
  265. Effect.void,
  266. () =>
  267. plugin.trigger(
  268. "aisdk.sdk",
  269. {
  270. model: model("google-vertex", "gemini", {
  271. endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
  272. }),
  273. package: "@ai-sdk/openai-compatible",
  274. options: { name: "google-vertex" },
  275. },
  276. {},
  277. ),
  278. () =>
  279. Effect.sync(() => {
  280. ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
  281. }),
  282. )
  283. expect(fetchCalls).toHaveLength(1)
  284. expect(fetchCalls[0].input).toBe("https://vertex.example")
  285. expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
  286. expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1")
  287. }),
  288. )
  289. it.effect("trims model IDs before selecting language models", () =>
  290. Effect.gen(function* () {
  291. const plugin = yield* PluginV2.Service
  292. const calls: string[] = []
  293. yield* plugin.add(GoogleVertexPlugin)
  294. yield* plugin.trigger(
  295. "aisdk.language",
  296. {
  297. model: model("google-vertex", " gemini-2.5-pro "),
  298. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  299. options: {},
  300. },
  301. {},
  302. )
  303. expect(calls).toEqual(["languageModel:gemini-2.5-pro"])
  304. }),
  305. )
  306. })