provider-google-vertex.test.ts 12 KB

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