From 5c8d46ab4be2f20797ceb4bea324de0cd8bf7023 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Tue, 18 Aug 2026 10:47:13 -0500 Subject: [PATCH] fix(core): make Google Vertex models work with ADC credentials (#43077) Signed-off-by: Major Hayden --- packages/core/src/plugin/models-dev.ts | 9 +++- .../core/src/plugin/provider/google-vertex.ts | 3 ++ packages/core/src/provider.ts | 11 +++++ packages/core/test/plugin/models-dev.test.ts | 40 ++++++++++++++++ .../plugin/provider-google-vertex.test.ts | 46 +++++++++++++++++++ packages/core/test/provider.test.ts | 20 ++++++++ 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 packages/core/test/provider.test.ts diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 4a87a3af3e8..2784fedc3f2 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -55,8 +55,13 @@ export const ModelsDevPlugin = define({ }) function environmentNames(provider: ModelsDev.Snapshot) { - if (provider.info.id !== Provider.ID.azure) return [...provider.environment] - return [...provider.environment.filter((name) => name.endsWith("_API_KEY")), "AZURE_COGNITIVE_SERVICES_API_KEY"] + if (provider.info.id === Provider.ID.azure) + return [...provider.environment.filter((name) => name.endsWith("_API_KEY")), "AZURE_COGNITIVE_SERVICES_API_KEY"] + // models.dev advertises project, location, and the ADC credentials file path for + // Vertex. Those configure Google auth rather than carrying a key, so only the + // Express Mode key may become a credential; GoogleVertexPlugin handles activation. + if (provider.info.id === Provider.ID.googleVertex) return ["GOOGLE_VERTEX_API_KEY"] + return [...provider.environment] } function snapshots(data: readonly ModelsDev.Snapshot[]) { diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index e91c9b029e2..d5fdc05951c 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -71,6 +71,9 @@ export const GoogleVertexPlugin = define({ const project = resolveProject(item.provider.settings ?? {}) const location = String(resolveLocation(item.provider.settings ?? {})) evt.provider.update(item.provider.id, (provider) => { + // Vertex authenticates through ADC rather than a key credential, so a + // resolvable project is what makes the provider usable. + if (project && provider.activation === "auto") provider.activation = "enabled" provider.settings = { ...provider.settings, ...(project ? { project } : {}), diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 70b1df53e35..0b121785275 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -48,6 +48,17 @@ const builtins = new Map Promise>([ ["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")], ["@opencode-ai/ai/providers/azure/responses", () => import("@opencode-ai/ai/providers/azure/responses")], ["@opencode-ai/ai/providers/google", () => import("@opencode-ai/ai/providers/google")], + ["@opencode-ai/ai/providers/google-vertex", () => import("@opencode-ai/ai/providers/google-vertex")], + ["@opencode-ai/ai/providers/google-vertex/gemini", () => import("@opencode-ai/ai/providers/google-vertex/gemini")], + ["@opencode-ai/ai/providers/google-vertex/chat", () => import("@opencode-ai/ai/providers/google-vertex/chat")], + [ + "@opencode-ai/ai/providers/google-vertex/responses", + () => import("@opencode-ai/ai/providers/google-vertex/responses"), + ], + [ + "@opencode-ai/ai/providers/google-vertex/messages", + () => import("@opencode-ai/ai/providers/google-vertex/messages"), + ], ["@opencode-ai/ai/providers/openai", () => import("@opencode-ai/ai/providers/openai")], ["@opencode-ai/ai/providers/openai/chat", () => import("@opencode-ai/ai/providers/openai/chat")], ["@opencode-ai/ai/providers/openai/responses", () => import("@opencode-ai/ai/providers/openai/responses")], diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 348ba4d3a83..f8c38c109f5 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -426,6 +426,46 @@ describe("ModelsDevPlugin", () => { }), ) + it.effect("advertises only key-bearing Google Vertex environment variables", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const catalog = yield* Catalog.Service + + yield* ModelsDevPlugin.effect( + host({ + catalog: catalogHost(catalog), + integration: integrationHost(integrations), + }), + ).pipe( + Effect.provideService( + ModelsDev.Service, + ModelsDev.Service.of({ + get: () => + Effect.succeed([ + { + info: { + id: Provider.ID.make("google-vertex"), + name: "Google Vertex", + activation: "auto", + package: Provider.aisdk("@ai-sdk/google-vertex"), + }, + environment: ["GOOGLE_VERTEX_PROJECT", "GOOGLE_VERTEX_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"], + models: [], + }, + ] satisfies readonly ModelsDev.Snapshot[]), + refresh: () => Effect.void, + }), + ), + ) + + // Vertex authenticates through ADC; project, location, and the credentials + // file path are configuration, not API keys. + expect(yield* integrations.get(Integration.ID.make("google-vertex"))).toMatchObject({ + methods: [{ type: "key" }, { type: "env", names: ["GOOGLE_VERTEX_API_KEY"] }], + }) + }), + ) + it.effect("converts reasoning options into settings variants", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 9ed9274189c..2acd2ac3005 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -141,6 +141,52 @@ describe("GoogleVertexPlugin", () => { ), ) + it.effect("enables the provider when a project resolves and leaves it automatic otherwise", () => + withEnv( + { + GOOGLE_VERTEX_PROJECT: undefined, + GOOGLE_CLOUD_PROJECT: undefined, + GCP_PROJECT: undefined, + GCLOUD_PROJECT: undefined, + }, + () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex") + }), + ) + yield* addPlugin() + + expect(required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))).activation).toBe("auto") + }), + ), + ) + + it.effect("enables the provider when a project resolves from env", () => + withEnv( + { + GOOGLE_VERTEX_PROJECT: undefined, + GOOGLE_CLOUD_PROJECT: "adc-project", + GCP_PROJECT: undefined, + GCLOUD_PROJECT: undefined, + }, + () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex") + }), + ) + yield* addPlugin() + + expect(required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))).activation).toBe("enabled") + }), + ), + ) + it.effect("resolves the advertised GOOGLE_VERTEX_PROJECT env for provider updates and SDKs", () => withEnv( { diff --git a/packages/core/test/provider.test.ts b/packages/core/test/provider.test.ts new file mode 100644 index 00000000000..af254cba274 --- /dev/null +++ b/packages/core/test/provider.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { Provider } from "@opencode-ai/core/provider" + +describe("Provider", () => { + test("loads Vertex native provider entrypoints", async () => { + const packages = [ + "@opencode-ai/ai/providers/google-vertex", + "@opencode-ai/ai/providers/google-vertex/gemini", + "@opencode-ai/ai/providers/google-vertex/chat", + "@opencode-ai/ai/providers/google-vertex/responses", + "@opencode-ai/ai/providers/google-vertex/messages", + ] + + for (const specifier of packages) { + const loaded = await Effect.runPromise(Provider.loadPackage(specifier)) + expect(loaded.model).toBeFunction() + } + }) +})